""" Plugin for making an asset library of all blender file found in a folder """ import re import uuid import os import shutil import json import time from pathlib import Path from itertools import groupby import bpy from bpy.props import (StringProperty, IntProperty, BoolProperty) from .library_plugin import LibraryPlugin from ..core.bl_utils import load_datablocks from ..core.template import Template class ScanFolder(LibraryPlugin): name = "Scan Folder" source_directory : StringProperty(subtype='DIR_PATH') source_template_file : StringProperty() source_template_image : StringProperty() source_template_video : StringProperty() source_template_info : StringProperty() def draw_prefs(self, layout): layout.prop(self, "source_directory", text="Source: Directory") col = layout.column(align=True) col.prop(self, "source_template_file", icon='COPY_ID', text='Template file') col.prop(self, "source_template_image", icon='COPY_ID', text='Template image') col.prop(self, "source_template_video", icon='COPY_ID', text='Template video') col.prop(self, "source_template_info", icon='COPY_ID', text='Template info') def get_asset_path(self, name, catalog, directory=None): directory = directory or self.source_directory catalog = self.norm_file_name(catalog) name = self.norm_file_name(name) return Path(directory, self.get_asset_relative_path(name, catalog)) def get_image_path(self, name, catalog, filepath): catalog = self.norm_file_name(catalog) name = self.norm_file_name(name) return self.format_path(self.source_template_image, dict(name=name, catalog=catalog, filepath=filepath)) def get_video_path(self, name, catalog, filepath): catalog = self.norm_file_name(catalog) name = self.norm_file_name(name) return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath)) def new_asset(self, asset, asset_data): raise Exception('Need to be defined in the plugin') def remove_asset(self, asset, asset_data): raise Exception('Need to be defined in the plugin') ''' def format_asset_info(self, asset_datas, asset_path, modified=None): asset_path = self.prop_rel_path(asset_path, 'source_directory') modified = modified or time.time_ns() library_id = self.library.id # if self.data_type == 'FILE': # return dict( # filepath=asset_path, # author=asset_info.get('author'), # modified=modified, # library_id=library_id, # catalog=asset_info['catalog'], # tags=[], # description=asset_info.get('description', ''), # type=self.data_type, # #image=self.source_template_image, # name=asset_info['name'] # ) return dict( filepath=asset_path, modified=modified, library_id=library_id, assets=[dict( catalog=asset_data['catalog'], author=asset_data.get('author', ''), metadata=asset_data.get('metadata', {}), description=asset_data.get('description', ''), tags=asset_data.get('tags', []), type=self.data_type, name=asset_data['name']) for asset_data in asset_datas ] ) ''' def set_asset_preview(self, asset, asset_cache): '''Load an externalize image as preview for an asset using the source template''' asset_path = self.format_path(asset_cache.filepath) image_template = self.source_template_image if not image_template: return image_path = self.find_path(image_template, asset_cache.to_dict(), filepath=asset_path) if image_path: with bpy.context.temp_override(id=asset): bpy.ops.ed.lib_id_load_custom_preview( filepath=str(image_path) ) else: print(f'No image found for {image_template} on {asset.name}') if asset.preview: return asset.preview def bundle(self, cache_diff=None): """Group all new assets in one or multiple blends for the asset browser""" if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'): print(f'{self.data_type} is not supported yet') return #catalog_data = self.read_catalog() catalog = self.read_catalog() cache = None if not cache_diff: # Get list of all modifications cache = self.fetch() cache_diff = cache.diff() # Write the cache in a temporary file for the generate preview script tmp_cache_file = cache.write(tmp=True) bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file)) elif isinstance(cache_diff, (Path, str)): cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8')) if self.library.blend_depth == 0: raise Exception('Blender depth must be 1 at min') total_assets = len(cache_diff) print(f'total_assets={total_assets}') if total_assets == 0: print('No assets found') return i = 0 for blend_path, asset_cache_diffs in cache_diff.group_by(key=self.get_asset_bundle_path): if blend_path.exists(): print(f'Opening existing bundle blend: {blend_path}') bpy.ops.wm.open_mainfile(filepath=str(blend_path)) else: print(f'Create new bundle blend to: {blend_path}') bpy.ops.wm.read_homefile(use_empty=True) for asset_cache_diff in asset_cache_diffs: if total_assets <= 100 or i % int(total_assets / 10) == 0: print(f'Progress: {int(i / total_assets * 100)+1}') operation = asset_cache_diff.operation asset_cache = asset_cache_diff.asset_cache asset_name = asset_cache.name asset = getattr(bpy.data, self.data_types).get(asset_name) if operation == 'REMOVE': if asset: getattr(bpy.data, self.data_types).remove(asset) else: print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}') continue if asset_cache_diff.operation == 'MODIFY' and not asset: print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created') if operation == 'ADD' or not asset: if asset: #raise Exception(f"Asset {asset_name} Already in Blend") print(f"Asset {asset_name} Already in Blend") getattr(bpy.data, self.data_types).remove(asset) #print(f"INFO: Add new asset: {asset_name}") asset = getattr(bpy.data, self.data_types).new(name=asset_name) else: print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFY)') continue asset.asset_mark() asset.asset_data.catalog_id = catalog.add(asset_cache_diff.catalog).id self.set_asset_preview(asset, asset_cache) self.set_asset_metadata(asset, asset_cache) self.set_asset_tags(asset, asset_cache) self.set_asset_info(asset, asset_cache) i += 1 print(f'Saving Blend to {blend_path}') blend_path.parent.mkdir(exist_ok=True, parents=True) bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True) # If the variable cache_diff was given we need to update the cache with the diff if cache is None: cache = self.read_cache() cache.update(cache_diff) cache.write() catalog.update(cache.catalogs) catalog.write() bpy.ops.wm.quit_blender() def fetch(self): """Gather in a list all assets found in the folder""" print(f'Fetch Assets for {self.library.name}') source_directory = Path(self.source_directory) template_file = Template(self.source_template_file) #catalog_data = self.read_catalog(directory=source_directory) #catalog_ids = {v['id']: k for k, v in catalog_data.items()} #self.catalog.read() cache = self.read_cache() print(f'Search for blend using glob template: {template_file.glob_pattern}') print(f'Scanning Folder {source_directory}...') #new_cache = LibraryCache() for asset_path in template_file.glob(source_directory): source_rel_path = self.prop_rel_path(asset_path, 'source_directory') modified = asset_path.stat().st_mtime_ns # Check if the asset description as already been cached file_cache = next((a for a in cache if a.filepath == source_rel_path), None) if file_cache: if file_cache.modified >= modified: #print(asset_path, 'is skipped because not modified') continue else: file_cache = cache.add(filepath=source_rel_path) rel_path = asset_path.relative_to(source_directory).as_posix() field_data = template_file.parse(rel_path) # Create the catalog path from the actual path of the asset catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)] #catalogs = [c.replace('_', ' ').title() for c in catalogs] asset_name = field_data.get('asset_name', asset_path.stem) if self.data_type == 'FILE': file_cache.set_data( name=asset_name, type='FILE', catalog=catalog, modified=modified ) continue # Now check if there is a asset description file (Commented for now propably not usefull) #asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path) #if asset_info_path: # new_cache.append(self.read_file(asset_info_path)) # continue # Scan the blend file for assets inside print(f'Scanning blendfile {asset_path}...') assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True) print(f'Found {len(assets)} {self.data_types} inside') for asset in assets: #catalog_path = catalog_ids.get(asset.asset_data.catalog_id) #if not catalog_path: # print(f'No catalog found for asset {asset.name}') #catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix() # For now the catalog used is the one extract from the template file file_cache.assets.add(self.get_asset_data(asset), catalog=catalog) getattr(bpy.data, self.data_types).remove(asset) return cache