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
30 BLENDERKIT_EXPORT_DATA_FILE
= "data.json"
32 from bpy
.props
import ( # TODO only keep the ones actually used when cleaning
37 from bpy
.types
import (
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(',')
53 for i
, s
in enumerate(commasep
):
60 def get_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
):
79 autothumb
.update_upload_model_preview(None, None)
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
):
97 autothumb
.update_upload_model_preview(None, None)
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
):
113 autothumb
.update_upload_material_preview(None, None)
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)
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
)
143 def camel_to_sub(content
):
144 replaced
= re
.sub(r
"[A-Z]", lambda m
: '_' + m
.group(0).lower(), content
)
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.
153 caller - upload operator or none
155 asset_type - asset type in capitals (blender enum)
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
173 # "type": asset_type,
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
)
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'}
210 "assetType": 'model',
214 "productionLevel": props
.production_level
.lower(),
215 "model_style": style
,
217 "modifiers": comma2array(props
.modifiers
),
218 "materials": comma2array(props
.materials
),
219 "shaders": comma2array(props
.shaders
),
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
,
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()
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
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'}
298 "assetType": 'scene',
302 "productionLevel": props
.production_level
.lower(),
303 "model_style": style
,
305 "modifiers": comma2array(props
.modifiers
),
306 "materials": comma2array(props
.materials
),
307 "shaders": comma2array(props
.shaders
),
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()
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()
358 "assetType": 'material',
363 "material_style": style
,
365 "shaders": comma2array(props
.shaders
),
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),
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...
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'
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)
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...
439 "textureResolutionMax": props
.texture_resolution_max
447 elif asset_type
== 'TEXTURE':
449 # if style == 'OTHER':
450 # style = props.style_other
453 "assetType": 'texture',
458 "animated": props
.animated
,
459 "purePbr": props
.pbr
,
460 "resolution": props
.resolution
,
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
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()
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
)
510 r
= rerequests
.patch(url
, json
=upload_data
, headers
=headers
, verify
=True) # files = files,
511 except requests
.exceptions
.RequestException
as e
:
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
:
552 profile
= bpy
.context
.window_manager
.get('bkit profile')
555 if utils
.profile_is_validator():
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']:
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)",
576 name
: StringProperty(
578 description
="Main name of the asset",
581 description
: StringProperty(
583 description
="Description of the asset",
585 tags
: StringProperty(
587 description
="List of tags, separated by commas (optional)",
590 category
: EnumProperty(
592 description
="main category to put into",
593 items
=categories
.get_category_enums
,
594 update
=categories
.update_category_enums
596 subcategory
: EnumProperty(
598 description
="main category to put into",
599 items
=categories
.get_subcategory_enums
,
600 update
= categories
.update_subcategory_enums
602 subcategory1
: EnumProperty(
604 description
="main category to put into",
605 items
=categories
.get_subcategory1_enums
607 license
: EnumProperty(
609 default
='royalty_free',
610 description
='License. Please read our help for choosing the right licenses',
612 is_private
: EnumProperty(
613 name
="Thumbnail Style",
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.",
623 free_full
:EnumProperty(
624 name
="Free or Full Plan",
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",
631 update
=update_free_full
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
):
642 # col = layout.column()
643 layout
.label(text
=self
.message
)
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
671 category
= self
.category
672 utils
.update_tags(self
, context
)
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
))
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,)))
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
])
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
):
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'])
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
:
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'
723 self
.is_private
= 'PUBLIC'
725 if asset_data
['isFree']:
726 self
.free_full
= 'FREE'
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
):
738 "verificationStatus": state
740 url
= paths
.get_api_url() + 'assets/' + str(asset_id
) + '/'
741 headers
= utils
.get_headers(api_key
)
743 r
= rerequests
.patch(url
, json
=upload_data
, headers
=headers
, verify
=True) # files = files,
744 except requests
.exceptions
.RequestException
as e
:
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.
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()
767 if ui_props
.asset_type
== 'SCENE':
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':
774 elif ui_props
.asset_type
== 'BRUSH':
779 def check_storage_quota(props
):
780 if props
.is_private
== 'PUBLIC':
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
)
788 props
.report
= 'Please log-in first.'
790 search
.write_profile(adata
)
792 quota
= profile
['user'].get('remainingPrivateQuota')
793 if quota
is None or quota
> 0:
795 props
.report
= 'Private storage quota exceeded.'
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
811 class Uploader(threading
.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
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()
833 self
._stop
_event
.set()
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
)
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'] == '':
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',)))
874 self
.send_message('uploaded metadata')
876 except requests
.exceptions
.RequestException
as e
:
882 url
+= self
.export_data
['id'] + '/'
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',)))
892 # print('uploaded metadata')
894 except requests
.exceptions
.RequestException
as e
:
900 self
.end_upload('Upload cancelled by user')
902 # props.upload_state = 'step 1'
903 if self
.upload_set
== ['METADATA']:
904 self
.end_upload('Metadata posted successfully')
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']
935 fpath
= os
.path
.join(self
.export_data
['temp_dir'], self
.upload_data
['assetBaseId'] + '.blend')
937 clean_file_path
= paths
.get_clean_filepath()
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([
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'],
967 "--python", os
.path
.join(script_path
, "upload_bg.py"),
969 ], bufsize
=1, stdout
=sys
.stdout
, stdin
=subprocess
.PIPE
, creationflags
=utils
.get_process_flags())
972 self
.end_upload('Upload stopped by user')
976 if 'THUMBNAIL' in self
.upload_set
:
980 "file_path": self
.export_data
["thumbnail_path"]
982 if 'MAINFILE' in self
.upload_set
:
989 if not os
.path
.exists(fpath
):
990 self
.send_message ("File packing failed, please try manual packing first")
993 self
.send_message('Uploading files')
995 uploaded
= upload_bg
.upload_files(self
.upload_data
, files
)
998 # mark on server as uploaded
999 if 'MAINFILE' in self
.upload_set
:
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')
1014 self
.end_upload('Upload failed')
1015 except Exception as 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.
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'}
1072 props
.asset_base_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"
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
)
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
)
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
):
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(
1135 description
="Type of upload",
1139 reupload
: BoolProperty(
1141 description
="reupload but also draw so that it asks what to reupload",
1143 options
={'SKIP_SAVE'}
1146 metadata
: BoolProperty(
1149 options
={'SKIP_SAVE'}
1152 thumbnail
: BoolProperty(
1155 options
={'SKIP_SAVE'}
1158 main_file
: BoolProperty(
1161 options
={'SKIP_SAVE'}
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
1181 if not self
.reupload
:
1182 upload_set
= ['METADATA', 'THUMBNAIL', 'MAINFILE']
1185 upload_set
.append('METADATA')
1187 upload_set
.append('THUMBNAIL')
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
, )
1200 def draw(self
, context
):
1201 props
= utils
.get_upload_props()
1202 layout
= self
.layout
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.'
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
)
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(
1247 def poll(cls
, context
):
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']
1262 if r
['id'] == self
.asset_id
:
1263 result
= r
.to_dict()
1266 if r
['id'] == self
.asset_id
:
1267 result
= r
.to_dict()
1269 ad
= bpy
.context
.active_object
.get('asset_data')
1271 result
= ad
.to_dict()
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))
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(
1291 state
: StringProperty(
1292 name
="verification_status",
1297 def poll(cls
, context
):
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']
1316 if r
['id'] == self
.asset_id
:
1317 r
['verificationStatus'] = self
.state
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
))
1327 def invoke(self
, context
, event
):
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
)