# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # This code is based on the Blenderkit Addon # Homepage: https://www.blenderkit.com/ # Sourcecode: https://github.com/blender/blender-addons/tree/master/blenderkit # # ##### # # This code is based on the Blenderkit Addon # Homepage: https://www.blenderkit.com/ # Sourcecode: https://github.com/blender/blender-addons/tree/master/blenderkit # # ##### import bpy import uuid from os.path import basename, dirname import hashlib import tempfile import os import urllib.error from mathutils import Vector, Matrix import threading from threading import _MainThread, Thread from ...handlers.lol.timer import timer_update from ...utils import get_addon_preferences, compatibility from ...utils.errorlog import LuxCoreErrorLog LOL_HOST_URL = "https://luxcorerender.org/lol" download_threads = [] def download_table_of_contents(context): scene = context.scene try: import urllib.request with urllib.request.urlopen(LOL_HOST_URL + "/assets_model.json", timeout=60) as request: import json scene.luxcoreOL.model['assets'] = json.loads(request.read()) for asset in scene.luxcoreOL.model['assets']: asset['downloaded'] = 0.0 # with urllib.request.urlopen(LOL_HOST_URL + "/assets_scene.json", timeout=60) as request: # import json # scene.luxcoreOL.scene['assets'] = json.loads(request.read()) # for asset in scene.luxcoreOL.scene['assets']: # asset['downloaded'] = 0.0 with urllib.request.urlopen(LOL_HOST_URL + "/assets_material.json", timeout=60) as request: import json scene.luxcoreOL.material['assets'] = json.loads(request.read()) for asset in scene.luxcoreOL.material['assets']: asset['downloaded'] = 0.0 context.scene.luxcoreOL.ui.ToC_loaded = True init_categories(context) bg_task = Thread(target=check_cache, args=(context, )) bg_task.start() return True except ConnectionError as error: print("Connection error: Could not download table of contents") print(error) return False except urllib.error.URLError as error: print("URL error: Could not download table of contents") print(error) return False def init_categories(context): scene = context.scene ui_props = scene.luxcoreOL.ui assets = get_search_props(context) categories = {} for asset in assets: cat = asset['category'] try: categories[cat] += 1 except KeyError: categories[cat] = 1 if ui_props.asset_type == 'MODEL': asset_props = scene.luxcoreOL.model if ui_props.asset_type == 'SCENE': asset_props = scene.luxcoreOL.scene if ui_props.asset_type == 'MATERIAL': asset_props = scene.luxcoreOL.material asset_props['categories'] = categories def check_cache(args): (context) = args name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences scene = context.scene assets = scene.luxcoreOL.model['assets'] for asset in assets: filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, "model", filename[:-3] + 'blend') if os.path.exists(filepath): if calc_hash(filepath) == asset["hash"]: asset['downloaded'] = 100.0 # assets = scene.luxcoreOL.scene['assets'] # for asset in assets: # filename = asset["url"] # filepath = os.path.join(user_preferences.global_dir, "scene", filename[:-3] + 'blend') # # if os.path.exists(filepath): # if calc_hash(filepath) == asset["hash"]: # asset['downloaded'] = 100.0 assets = scene.luxcoreOL.material['assets'] for asset in assets: filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, "material", filename[:-3] + 'blend') if os.path.exists(filepath): if calc_hash(filepath) == asset["hash"]: asset['downloaded'] = 100.0 def calc_hash(filename): BLOCK_SIZE = 65536 file_hash = hashlib.sha256() with open(filename, 'rb') as file: block = file.read(BLOCK_SIZE) while len(block) > 0: file_hash.update(block) block = file.read(BLOCK_SIZE) return file_hash.hexdigest() def is_downloading(asset): global download_threads for thread_data in download_threads: if thread_data[2].passargs['thumbnail']: continue if asset['hash'] == thread_data[1]['hash']: # print(asset["name"], "is downloading") return thread_data[2] return None def download_file(asset_type, asset, location, rotation, target_object, target_slot): downloader = {'location': (location[0],location[1],location[2]), 'rotation': (rotation[0],rotation[1],rotation[2]), 'target_object': target_object, 'target_slot': target_slot} tcom = is_downloading(asset) if tcom is None: tcom = ThreadCom() tcom.passargs['downloaders'] = [downloader] tcom.passargs['thumbnail'] = False tcom.passargs['asset type'] = asset_type asset_data = asset.to_dict() downloadthread = Downloader(asset_data, tcom) download_threads.append([downloadthread, asset_data, tcom]) bpy.app.timers.register(timer_update) else: tcom.passargs['downloaders'].append(downloader) return True class Downloader(threading.Thread): def __init__(self, asset, tcom): super(Downloader, self).__init__() self.asset = asset self.tcom = tcom self._stop_event = threading.Event() def stop(self): # print("Download Thread stopped") self._stop_event.set() def stopped(self): return self._stop_event.is_set() # def main_download_thread(asset_data, tcom, scene_id, api_key): def run(self): import urllib.request user_preferences = get_addon_preferences(bpy.context) # print("Download Thread running") tcom = self.tcom if tcom.passargs['thumbnail']: # Thumbnail download imagename = self.asset['url'][:-4] + '.jpg' thumbnailpath = os.path.join(user_preferences.global_dir, tcom.passargs['asset type'].lower(), "preview", imagename) url = LOL_HOST_URL + "/" + tcom.passargs['asset type'].lower() + "/preview/" + imagename try: with urllib.request.urlopen(url, timeout=60) as url_handle, open(thumbnailpath, "wb") as file_handle: file_handle.write(url_handle.read()) imgname = self.asset['thumbnail'] img = bpy.data.images.load(thumbnailpath) img.name = imgname img.colorspace_settings.name = 'Linear' tcom.finished = True except ConnectionError as error: print("Connection error: Could not download " + imagename) print(error) except urllib.error.HTTPError as error: print("HTTPError error: Could not download " + imagename) print(error) else: #Asset download filename = self.asset["url"] with tempfile.TemporaryDirectory() as temp_dir_path: temp_zip_path = os.path.join(temp_dir_path, filename) url = LOL_HOST_URL + "/" + tcom.passargs['asset type'].lower() + "/" + filename try: print("Downloading:", url) with urllib.request.urlopen(url, timeout=60) as url_handle, \ open(temp_zip_path, "wb") as file_handle: total_length = url_handle.headers.get('Content-Length') tcom.file_size = int(total_length) dl = 0 data = url_handle.read(8192) file_handle.write(data) while len(data) == 8192: data = url_handle.read(8192) dl += len(data) tcom.downloaded = dl tcom.progress = int(100 * tcom.downloaded / tcom.file_size) # Stop download if Blender is closed for thread in threading.enumerate(): if isinstance(thread, _MainThread): if not thread.is_alive(): self.stop() file_handle.write(data) if self.stopped(): url_handle.close() return print("Download finished") import zipfile with zipfile.ZipFile(temp_zip_path) as zf: print("Extracting zip to", os.path.join(user_preferences.global_dir, tcom.passargs['asset type'].lower())) zf.extractall(os.path.join(user_preferences.global_dir, tcom.passargs['asset type'].lower())) tcom.finished = True except urllib.error.URLError as err: print("Could not download: %s" % err) class ThreadCom: # object passed to threads to read background process stdout info def __init__(self): self.file_size = 1000000000000000 # property that gets written to. self.downloaded = 0 self.progress = 0.0 self.finished = False self.passargs = {} def link_asset(context, asset, location, rotation): name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, "model", filename[:-3] + 'blend') scene = context.scene link_model = (scene.luxcoreOL.model.append_method == 'LINK_COLLECTION') with bpy.data.libraries.load(filepath, link=link_model) as (data_from, data_to): data_to.objects = [name for name in data_from.objects if name not in ["Plane", "Camera"]] bbox_min = asset["bbox_min"] bbox_max = asset["bbox_max"] bbox_center = 0.5 * Vector((bbox_max[0] + bbox_min[0], bbox_max[1] + bbox_min[1], 0.0)) # TODO: Check if asset is already used in scene and override append/link selection # If the same model is first linked and then appended it breaks relationships and transformaton in blender # Add new collection, where the assets are placed into col = bpy.data.collections.new(asset["name"]) # Add parent empty for asset collection main_object = bpy.data.objects.new(asset["name"], None) main_object.empty_display_size = 0.5 * max(bbox_max[0] - bbox_min[0], bbox_max[1] - bbox_min[1], bbox_max[2] - bbox_min[2]) main_object.location = location main_object.rotation_euler = rotation main_object.empty_display_size = 0.5*max(bbox_max[0] - bbox_min[0], bbox_max[1] - bbox_min[1], bbox_max[2] - bbox_min[2]) if link_model: main_object.instance_type = 'COLLECTION' main_object.instance_collection = col col.instance_offset = bbox_center else: scene.collection.children.link(col) scene.collection.objects.link(main_object) # Objects have to be linked to show up in a scene for obj in data_to.objects: if not link_model: obj.data.make_local() parent = obj while parent.parent != None: parent = parent.parent if parent != main_object: parent.parent = main_object parent.matrix_parent_inverse = main_object.matrix_world.inverted() @ Matrix.Translation(-1*bbox_center) # Add objects to asset collection col.objects.link(obj) compatibility.run() def append_material(context, asset, target_object, target_slot): if target_object == None: return name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, "material", filename[:-3] + 'blend') with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): data_to.materials = [name for name in data_from.materials if name == asset["name"]] if len(data_to.materials) == 1: # print(target_object, target_slot, data_to.materials[0].name) if len(bpy.data.objects[target_object].material_slots) == 0: bpy.data.objects[target_object].data.materials.append(data_to.materials[0]) else: if bpy.data.objects[target_object].library == None: bpy.data.objects[target_object].material_slots[target_slot].material = data_to.materials[0] compatibility.run() def load_asset(context, asset, location, rotation, target_object, target_slot): name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences ui_props = context.scene.luxcoreOL.ui #TODO: write method for this as it is used serveral times if ui_props.asset_type == 'SCENE': filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, "model", filename[:-3] + 'blend') else: filename = asset["url"] filepath = os.path.join(user_preferences.global_dir, ui_props.asset_type.lower(), filename[:-3] + 'blend') ''' Check if model is cached ''' download = False if not os.path.exists(filepath): download = True else: hash = calc_hash(filepath) if hash != asset["hash"]: print("hash number doesn't match: %s" % hash) download = True if download: print("Download asset") download_file(ui_props.asset_type, asset, location, rotation, target_object, target_slot) else: if ui_props.asset_type == 'MATERIAL': append_material(context, asset, target_object, target_slot) else: link_asset(context, asset, location, rotation) def get_search_props(context): scene = context.scene if scene is None: return ui_props = scene.luxcoreOL.ui props = None if ui_props.asset_type == 'MODEL': if not 'assets' in scene.luxcoreOL.model: return props = scene.luxcoreOL.model['assets'] if ui_props.asset_type == 'SCENE': if not 'assets' in scene.luxcoreOL.scene: return props = scene.luxcoreOL.scene['assets'] if ui_props.asset_type == 'MATERIAL': if not 'assets' in scene.luxcoreOL.material: return props = scene.luxcoreOL.material['assets'] # if ui_props.asset_type == 'TEXTURE': # if not hasattr(scene.luxcoreOL.texture, 'assets'): # return # props = scene.luxcoreOL.texture['assets'] # if ui_props.asset_type == 'BRUSH': # if not hasattr(scene.luxcoreOL, 'brush'): # return # props = scene.luxcoreOL.brush['assets'] return props def save_prefs(self, context): # first check context, so we don't do this on registration or blender startup if not bpy.app.background: #(hasattr kills blender) name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences # TODO: Implement test = 1 #prefs = { # 'global_dir': user_preferences.global_dir, #} #try: # fpath = paths.BLENDERKIT_SETTINGS_FILENAME # if not os.path.exists(paths._presets): # os.makedirs(paths._presets) # f = open(fpath, 'w') # with open(fpath, 'w') as s: # import json # json.dump(prefs, s) #except Exception as e: # print(e) def get_default_directory(): from os.path import expanduser home = expanduser("~") return home + os.sep + 'LuxCoreOnlineLibrary_data' def get_scene_id(): '''gets scene id and possibly also generates a new one''' bpy.context.scene['uuid'] = bpy.context.scene.get('uuid', str(uuid.uuid4())) return bpy.context.scene['uuid'] def guard_from_crash(): '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.''' #if bpy.context.preferences.addons.get('BlendLuxCore') is None: # return False #if bpy.context.preferences.addons['BlendLuxCore'].preferences is None: # return False return True def download_thumbnail(self, context, asset, index): ui_props = context.scene.luxcoreOL.ui tcom = is_downloading(asset) if tcom is None: tcom = ThreadCom() tcom.passargs['thumbnail'] = True tcom.passargs['asset type'] = ui_props.asset_type downloadthread = Downloader(asset, tcom) download_threads.append([downloadthread, asset, tcom]) bpy.app.timers.register(timer_update) return True def get_thumbnail(imagename): name = dirname(dirname(dirname(__file__))) path = os.path.join(name, 'thumbnails', imagename) imagename = '.%s' % imagename img = bpy.data.images.get(imagename) if img == None: img = bpy.data.images.load(path) img.colorspace_settings.name = 'Linear' img.name = imagename img.name = imagename return img def previmg_name(index, fullsize=False): if not fullsize: return '.LOL_preview_'+ str(index).zfill(2) else: return '.LOL_preview_full_' + str(index).zfill(2) def load_previews(context, assets): name = basename(dirname(dirname(dirname(__file__)))) user_preferences = context.preferences.addons[name].preferences ui_props = context.scene.luxcoreOL.ui if assets is not None and len(assets) != 0: i = 0 for asset in assets: if ui_props.asset_type == 'MATERIAL': tpath = os.path.join(user_preferences.global_dir, ui_props.asset_type.lower(), "preview", asset['name'] + '.jpg') else: tpath = os.path.join(user_preferences.global_dir, ui_props.asset_type.lower(), "preview", asset['url'][:-4] + '.jpg') imgname = previmg_name(i) asset["thumbnail"] = imgname if os.path.exists(tpath): img = bpy.data.images.get(imgname) if img is None or img.size[0] == 0: img = bpy.data.images.load(tpath) img.name = imgname elif img.filepath != tpath: # had to add this check for autopacking files... if img.packed_file is not None: img.unpack(method='USE_ORIGINAL') img.filepath = tpath img.reload() img.colorspace_settings.name = 'Linear' else: if imgname in bpy.data.images: img = bpy.data.images[imgname] bpy.data.images.remove(img) # print('Thumbnail not cached: ', imgname) download_thumbnail(None, context, asset, i) i += 1