First Commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
|
||||
from asset_library.adapters.adapter import AssetLibraryAdapter
|
||||
from asset_library.adapters.copy_folder import CopyFolderLibrary
|
||||
from asset_library.adapters.scan_folder import ScanFolderLibrary
|
||||
@@ -0,0 +1,545 @@
|
||||
|
||||
from asset_library.common.functions import (read_catalog, write_catalog, norm_asset_datas, get_catalog_path)
|
||||
from asset_library.common.bl_utils import get_addon_prefs, load_datablocks
|
||||
from asset_library.common.file_utils import read_file, write_file
|
||||
from asset_library.common.template import Template
|
||||
|
||||
from asset_library import (action, collection, file)
|
||||
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import StringProperty
|
||||
import bpy
|
||||
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
class AssetLibraryAdapter(PropertyGroup):
|
||||
|
||||
#def __init__(self):
|
||||
name = "Base Adapter"
|
||||
#library = None
|
||||
|
||||
bundle_directory : StringProperty()
|
||||
|
||||
@property
|
||||
def library(self):
|
||||
prefs = self.addon_prefs
|
||||
for lib in prefs.libraries:
|
||||
if lib.adapter == self:
|
||||
return lib
|
||||
if lib.conform.adapter == self:
|
||||
return lib
|
||||
|
||||
@property
|
||||
def library_path(self):
|
||||
return self.library.library_path
|
||||
|
||||
@property
|
||||
def image_template(self):
|
||||
return Template(self.library.image_template)
|
||||
|
||||
@property
|
||||
def video_template(self):
|
||||
return Template(self.library.video_template)
|
||||
|
||||
@property
|
||||
def asset_description_template(self):
|
||||
return Template(self.library.asset_description_template)
|
||||
|
||||
@property
|
||||
def data_type(self):
|
||||
return self.library.data_type
|
||||
|
||||
@property
|
||||
def data_types(self):
|
||||
return self.library.data_types
|
||||
|
||||
@property
|
||||
def blend_depth(self):
|
||||
return self.library.blend_depth
|
||||
|
||||
@property
|
||||
def externalize_data(self):
|
||||
return self.library.externalize_data
|
||||
|
||||
@property
|
||||
def catalog_path(self):
|
||||
return self.library.catalog_path
|
||||
|
||||
def get_catalog_path(self, filepath):
|
||||
return get_catalog_path(filepath)
|
||||
|
||||
@property
|
||||
def cache_file(self):
|
||||
return Path(self.library_path) / f"blender_assets.{self.library.id}.json"
|
||||
#return get_asset_datas_file(self.library_path)
|
||||
|
||||
@property
|
||||
def addon_prefs(self):
|
||||
return get_addon_prefs()
|
||||
|
||||
@property
|
||||
def module_type(self):
|
||||
lib_type = self.library.data_type
|
||||
if lib_type == 'ACTION':
|
||||
return action
|
||||
elif lib_type == 'FILE':
|
||||
return file
|
||||
elif lib_type == 'COLLECTION':
|
||||
return collection
|
||||
|
||||
def to_dict(self):
|
||||
return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
|
||||
|
||||
def fetch(self):
|
||||
raise Exception('This method need to be define in the adapter')
|
||||
|
||||
def norm_file_name(self, name):
|
||||
return name.replace(' ', '_')
|
||||
|
||||
def copy_file(self, source, destination):
|
||||
src = Path(source)
|
||||
dst = Path(destination)
|
||||
|
||||
if not source.exists():
|
||||
print(f'Cannot copy file {source}: file not exist')
|
||||
return
|
||||
|
||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
if src == dst:
|
||||
print(f'Cannot copy file {source}: source and destination are the same')
|
||||
return
|
||||
|
||||
print(f'Copy file from {source} to {destination}')
|
||||
shutil.copy2(str(source), str(destination))
|
||||
|
||||
def load_datablocks(self, src, names=None, type='objects', link=True, expr=None):
|
||||
"""Link or append a datablock from a blendfile"""
|
||||
return load_datablocks(src, names=names, type=type, link=link, expr=expr)
|
||||
|
||||
def get_asset_relative_path(self, name, catalog):
|
||||
'''Get a relative path for the asset'''
|
||||
name = self.norm_file_name(name)
|
||||
return Path(catalog, name, name).with_suffix('.blend')
|
||||
|
||||
#def _get_file_name(self, name, filepath):
|
||||
# '''Ensure having a unique name per asset if in the same folder by prefixing with the blend_file name'''
|
||||
# file_name = name
|
||||
# if filepath.stem != name:
|
||||
# file_name = f'{file_name}_{name}'
|
||||
#
|
||||
# return file_name
|
||||
|
||||
def get_active_asset_library(self):
|
||||
asset_handle = bpy.context.asset_file_handle
|
||||
prefs = get_addon_prefs()
|
||||
asset_handle = bpy.context.asset_file_handle
|
||||
|
||||
lib = None
|
||||
if '.library_id' in asset_handle.asset_data:
|
||||
lib_id = asset_handle.asset_data['.library_id']
|
||||
lib = next((l for l in prefs.libraries if l.id == lib_id), None)
|
||||
|
||||
if not lib:
|
||||
print(f"No library found for id {lib_id}")
|
||||
|
||||
if not lib:
|
||||
lib = self
|
||||
|
||||
return lib
|
||||
|
||||
def get_active_asset_path(self):
|
||||
'''Get the full path of the active asset_handle from the asset brower'''
|
||||
prefs = get_addon_prefs()
|
||||
asset_handle = bpy.context.asset_file_handle
|
||||
|
||||
lib = self.get_active_asset_library()
|
||||
|
||||
if 'filepath' in asset_handle.asset_data:
|
||||
asset_path = asset_handle.asset_data['filepath']
|
||||
asset_path = lib.adapter.format_path(asset_path)
|
||||
else:
|
||||
asset_path = bpy.types.AssetHandle.get_full_library_path(
|
||||
asset_handle, bpy.context.asset_library_ref
|
||||
)
|
||||
|
||||
return asset_path
|
||||
|
||||
def get_path(self, type, name, asset_path, template=None) -> Path:
|
||||
if not template:
|
||||
template = getattr(self, f'{type}_template')
|
||||
|
||||
if isinstance(template, str):
|
||||
template = Template(template)
|
||||
|
||||
filepath = Path(asset_path)
|
||||
return (filepath / template.format(name=name, path=Path(asset_path))).resolve()
|
||||
|
||||
#def get_image_path(self, name, asset_path):
|
||||
# filepath = Path(asset_path)
|
||||
# image_name = self._get_file_name(name, asset_path)
|
||||
# return (filepath / self.image_template.format(name=image_name)).resolve()
|
||||
|
||||
def get_cache_image_path(self, name, catalog) -> Path:
|
||||
""""Get the the cache path of a image for asset without an externalized image"""
|
||||
return Path(self.library_path, '.previews', f"{catalog.replace('/', '_')}_{name}").with_suffix(('.png'))
|
||||
|
||||
def get_cache_image(self, name, catalog):
|
||||
cache_image_path = self.get_cache_image_path(name, catalog)
|
||||
if cache_image_path.exists():
|
||||
return cache_image_path
|
||||
|
||||
#def get_video_path(self, name, asset_path):
|
||||
# filepath = Path(asset_path)
|
||||
# video_name = self._get_file_name(name, asset_path)
|
||||
# return (filepath / self.video_template.format(name=video_name)).resolve()
|
||||
|
||||
def get_image(self, name, asset_path):
|
||||
image_path = self.get_path('image', name, asset_path)
|
||||
if image_path.exists():
|
||||
return image_path
|
||||
|
||||
def get_video(self, name, asset_path):
|
||||
video_path = self.get_path('video', name, asset_path)
|
||||
if video_path.exists():
|
||||
return video_path
|
||||
|
||||
def get_asset_description_path(self, asset_path) -> Path:
|
||||
""""Get the path of the json or yaml describing all assets data in onle file"""
|
||||
filepath = Path(asset_path)
|
||||
return (filepath / self.asset_description_template.format(name=filepath.stem)).resolve()
|
||||
|
||||
def read_asset_description(self, asset_path) -> dict:
|
||||
"""Read the description file of the asset"""
|
||||
|
||||
asset_description_path = self.get_asset_description_path(asset_path)
|
||||
return read_file(asset_description_path)
|
||||
|
||||
def write_asset_description(self, asset_data, asset_path) -> None:
|
||||
asset_description_path = self.get_asset_description_path(asset_path)
|
||||
return write_file(asset_description_path, asset_data)
|
||||
|
||||
def write_asset(self, asset, asset_path):
|
||||
bpy.data.libraries.write(
|
||||
str(asset_path),
|
||||
{asset},
|
||||
path_remap="NONE",
|
||||
fake_user=True,
|
||||
compress=True
|
||||
)
|
||||
|
||||
|
||||
def read_catalog(self, filepath=None):
|
||||
"""Read the catalog file of the library bundle path or of the specified filepath"""
|
||||
|
||||
catalog_path = self.catalog_path
|
||||
if filepath:
|
||||
catalog_path = self.get_catalog_path(filepath)
|
||||
return read_catalog(catalog_path)
|
||||
|
||||
def write_catalog(self, catalog_data, filepath=None):
|
||||
"""Write the catalog file in the library bundle path or of the specified filepath"""
|
||||
|
||||
catalog_path = self.catalog_path
|
||||
if filepath:
|
||||
catalog_path = self.get_catalog_path(filepath)
|
||||
|
||||
return write_catalog(catalog_path, catalog_data)
|
||||
|
||||
def read_cache(self):
|
||||
return read_file(self.cache_file)
|
||||
|
||||
def norm_asset_datas(self, asset_file_datas):
|
||||
''' Return a new flat list of asset data
|
||||
the filepath keys are merge with the assets keys
|
||||
'''
|
||||
return norm_asset_datas(asset_file_datas)
|
||||
|
||||
def write_cache(self, asset_datas):
|
||||
path = self.cache_file
|
||||
print(f'cache file writen to {path}')
|
||||
return write_file(path, list(asset_datas))
|
||||
|
||||
def prop_rel_path(self, path, prop):
|
||||
'''Get a filepath relative to a property of the adapter'''
|
||||
field_prop = '{%s}/'%prop
|
||||
|
||||
prop_value = getattr(self, prop)
|
||||
prop_value = Path(os.path.expandvars(prop_value)).resolve()
|
||||
|
||||
rel_path = Path(path).resolve().relative_to(prop_value).as_posix()
|
||||
|
||||
return field_prop + rel_path
|
||||
|
||||
def write_preview(self, preview, filepath):
|
||||
if not preview or not filepath:
|
||||
return
|
||||
|
||||
filepath = Path(filepath)
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
img_size = preview.image_size
|
||||
|
||||
px = [0] * img_size[0] * img_size[1] * 4
|
||||
preview.image_pixels_float.foreach_get(px)
|
||||
img = bpy.data.images.new(name=filepath.name, width=img_size[0], height=img_size[1], is_data=True, alpha=True)
|
||||
img.pixels.foreach_set(px)
|
||||
img.filepath_raw = str(filepath.with_suffix('.png'))
|
||||
img.file_format = 'PNG'
|
||||
img.save()
|
||||
|
||||
def draw_header(self, layout):
|
||||
"""Draw the header of the Asset Browser Window"""
|
||||
#layout.separator()
|
||||
|
||||
self.module_type.gui.draw_header(layout)
|
||||
|
||||
def draw_context_menu(self, layout):
|
||||
"""Draw the context menu of the Asset Browser Window"""
|
||||
#layout.separator()
|
||||
self.module_type.gui.draw_context_menu(layout)
|
||||
|
||||
def group_key(self, asset_data):
|
||||
"""Key used to group assets inside one blend"""
|
||||
|
||||
catalog_parts = asset_data['catalog'].split('/') + [asset_data['name']]
|
||||
|
||||
return catalog_parts[:self.blend_depth]
|
||||
|
||||
def set_asset_preview(self, asset, asset_data):
|
||||
"""Load an externalize image as preview for an asset"""
|
||||
|
||||
image_path = Path(asset_data['image'])
|
||||
if not image_path.is_absolute():
|
||||
image_path = Path(asset_data['filepath'], image_path)
|
||||
|
||||
image_path = self.format_path(image_path.as_posix())
|
||||
if image_path and image_path.exists():
|
||||
with bpy.context.temp_override(id=asset):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=str(image_path)
|
||||
)
|
||||
return
|
||||
|
||||
if asset.preview:
|
||||
return
|
||||
|
||||
#Creating the preview for collection, object or material
|
||||
src_asset = self.load_datablocks(asset_data['filepath'], names=asset_data['name'], link=True, type=self.data_types)
|
||||
if not src_asset:
|
||||
print(f'No asset named {asset_data["name"]} in {asset_data["filepath"]}')
|
||||
return
|
||||
|
||||
bpy.ops.ed.lib_id_generate_preview({"id": src_asset})
|
||||
|
||||
#Transfering pixels between previews
|
||||
pixels = [0] * (128*128*4)
|
||||
src_asset.preview.image_pixels_float.foreach_get(pixels)
|
||||
asset.preview.image_pixels_float.foreach_set(pixels)
|
||||
|
||||
getattr(bpy.data, self.data_types).remove(src_asset)
|
||||
|
||||
|
||||
def set_asset_catalog(self, asset, asset_data, catalog_data):
|
||||
"""Find the catalog if already exist or create it"""
|
||||
catalog_name = asset_data['catalog']
|
||||
catalog = catalog_data.get(catalog_name)
|
||||
if not catalog:
|
||||
catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
||||
catalog_data[catalog_name] = catalog
|
||||
|
||||
asset.asset_data.catalog_id = catalog['id']
|
||||
|
||||
def set_asset_metadata(self, asset, asset_data):
|
||||
"""Create custom prop to an asset base on provided data"""
|
||||
metadata = asset_data.get('metadata', {})
|
||||
|
||||
library_id = self.library.id
|
||||
if 'library_id' in asset_data:
|
||||
library_id = asset_data['library_id']
|
||||
|
||||
metadata['.library_id'] = library_id
|
||||
metadata['filepath'] = asset_data['filepath']
|
||||
for k, v in metadata.items():
|
||||
asset.asset_data[k] = v
|
||||
|
||||
def set_asset_tags(self, asset, asset_data):
|
||||
"""Create asset tags base on provided data"""
|
||||
tags = asset_data.get('tags', [])
|
||||
if tags:
|
||||
#Clear all tags first
|
||||
for tag in asset.asset_data.tags[:]:
|
||||
asset.asset_data.tags.remove(tag)
|
||||
|
||||
for tag in tags:
|
||||
if not tag:
|
||||
continue
|
||||
asset.asset_data.tags.new(tag, skip_if_exists=True)
|
||||
|
||||
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
|
||||
|
||||
lib_path = self.library_path
|
||||
catalog_data = self.read_catalog() #TODO remove unused catalog
|
||||
|
||||
write_cache = False
|
||||
if not cache_diff:
|
||||
# Get list of all modifications
|
||||
cache, cache_diff = self.diff()
|
||||
|
||||
# Only write complete cache at the end
|
||||
write_cache = True
|
||||
|
||||
elif isinstance(cache_diff, (Path, str)):
|
||||
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))
|
||||
|
||||
if self.blend_depth == 0:
|
||||
groups = [(cache_diff)]
|
||||
else:
|
||||
cache_diff.sort(key=self.group_key)
|
||||
groups = groupby(cache_diff, key=self.group_key)
|
||||
|
||||
total_assets = len(cache_diff)
|
||||
print(f'total_assets={total_assets}')
|
||||
|
||||
if total_assets == 0:
|
||||
print('No assets found')
|
||||
return
|
||||
|
||||
i = 0
|
||||
for sub_path, asset_datas in groups:
|
||||
blend_name = sub_path[-1].replace(' ', '_').lower()
|
||||
blend_path = Path(lib_path, *sub_path, blend_name).with_suffix('.blend')
|
||||
|
||||
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_data in asset_datas:
|
||||
if total_assets <= 100 or i % int(total_assets / 10) == 0:
|
||||
print(f'Progress: {int(i / total_assets * 100)+1}')
|
||||
|
||||
operation = asset_data.get('operation', 'ADD')
|
||||
asset = getattr(bpy.data, self.data_types).get(asset_data['name'])
|
||||
|
||||
if operation == 'REMOVE':
|
||||
if asset:
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
else:
|
||||
print(f'ERROR : Remove Asset: {asset_data["name"]} not found in {blend_path}')
|
||||
continue
|
||||
|
||||
if operation == 'MODIFY' and not asset:
|
||||
print(f'WARNING: Modifiy Asset: {asset_data["name"]} not found in {blend_path} it will be created')
|
||||
|
||||
elif operation == 'ADD' or not asset:
|
||||
if asset:
|
||||
#raise Exception(f"Asset {asset_data['name']} Already in Blend")
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
#print(f"INFO: Add new asset: {asset_data['name']}")
|
||||
asset = getattr(bpy.data, self.data_types).new(name=asset_data['name'])
|
||||
else:
|
||||
print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFIED)')
|
||||
continue
|
||||
|
||||
asset.asset_mark()
|
||||
|
||||
self.set_asset_preview(asset, asset_data)
|
||||
#if self.externalize_data:
|
||||
# self.write_preview(preview, filepath)
|
||||
|
||||
self.set_asset_catalog(asset, asset_data, catalog_data)
|
||||
self.set_asset_metadata(asset, asset_data)
|
||||
self.set_asset_tags(asset, asset_data)
|
||||
asset.asset_data.description = asset_data.get('description', '')
|
||||
|
||||
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 write_cache:
|
||||
self.write_cache(cache)
|
||||
|
||||
self.write_catalog(catalog_data)
|
||||
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
|
||||
def norm_cache(self, cache):
|
||||
""" Return a new flat list of asset data
|
||||
the filepath keys are merge with the assets keys"""
|
||||
|
||||
new_cache = []
|
||||
for asset_description in cache:
|
||||
asset_description = asset_description.copy()
|
||||
if 'assets' in asset_description:
|
||||
|
||||
assets = asset_description.pop('assets')
|
||||
for asset_data in assets:
|
||||
new_cache.append({**asset_description, **asset_data})
|
||||
else:
|
||||
new_cache.append(asset_description)
|
||||
|
||||
return new_cache
|
||||
|
||||
def diff(self):
|
||||
"""Compare the library cache with it current state and return the difference"""
|
||||
|
||||
cache = self.read_cache()
|
||||
|
||||
if cache is None:
|
||||
print(f'Fetch The library {self.library.name} for the first time, might be long...')
|
||||
cache = []
|
||||
|
||||
new_cache = self.fetch()
|
||||
|
||||
cache = {f"{a['filepath']}/{a['name']}": a for a in self.norm_cache(cache)}
|
||||
new_cache = {f"{a['filepath']}/{a['name']}" : a for a in self.norm_cache(new_cache)}
|
||||
|
||||
assets_added = [v for k, v in new_cache.items() if k not in cache]
|
||||
assets_removed = [v for k, v in cache.items() if k not in new_cache]
|
||||
assets_modified = [v for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
|
||||
|
||||
if assets_added:
|
||||
print(f'{len(assets_added)} Assets Added \n{tuple(a["name"] for a in assets_added[:10])}\n')
|
||||
if assets_removed:
|
||||
print(f'{len(assets_removed)} Assets Removed \n{tuple(a["name"] for a in assets_removed[:10])}\n')
|
||||
if assets_modified:
|
||||
print(f'{len(assets_modified)} Assets Modified \n{tuple(a["name"] for a in assets_modified[:10])}\n')
|
||||
|
||||
assets_added = [dict(a, operation='ADD') for a in assets_added]
|
||||
assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
|
||||
assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
|
||||
|
||||
cache_diff = assets_added + assets_removed + assets_modified
|
||||
if not cache_diff:
|
||||
print('No change in the library')
|
||||
|
||||
return new_cache, cache_diff
|
||||
|
||||
def draw_prefs(self, layout):
|
||||
"""Draw the options in the addon preference for this adapter"""
|
||||
|
||||
annotations = self.__class__.__annotations__
|
||||
for k, v in annotations.items():
|
||||
layout.prop(self, k, text=bpy.path.display_name(k))
|
||||
|
||||
def format_path(self, template, **kargs):
|
||||
return Template(template).format(self.to_dict(), **kargs).resolve()
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
"""
|
||||
Adapter for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
|
||||
from asset_library.adapters.adapter import AssetLibraryAdapter
|
||||
from asset_library.common.file_utils import copy_dir
|
||||
from bpy.props import StringProperty
|
||||
from os.path import expandvars
|
||||
|
||||
|
||||
class CopyFolderLibrary(AssetLibraryAdapter):
|
||||
"""Copy library folder from a server to a local disk for better performance"""
|
||||
|
||||
name = "Copy Folder"
|
||||
source_directory : StringProperty()
|
||||
|
||||
includes : StringProperty()
|
||||
excludes : StringProperty()
|
||||
|
||||
def bundle(self):
|
||||
src = expandvars(self.source_directory)
|
||||
dst = expandvars(self.library_path)
|
||||
|
||||
includes = [inc.strip() for inc in self.includes.split(',')]
|
||||
excludes = [ex.strip() for ex in self.excludes.split(',')]
|
||||
|
||||
print(f'Copy Folder from {src} to {dst}...')
|
||||
copy_dir(
|
||||
src, dst, only_recent=True,
|
||||
excludes=excludes, includes=includes
|
||||
)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
|
||||
from asset_library.adapters.adapter import AssetLibraryAdapter
|
||||
from asset_library.common.template import Template
|
||||
from asset_library.common.file_utils import install_module
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
||||
import re
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import urllib3
|
||||
import traceback
|
||||
import time
|
||||
|
||||
gazu = install_module('gazu')
|
||||
|
||||
|
||||
class KitsuLibrary(AssetLibraryAdapter):
|
||||
|
||||
name = "Kitsu"
|
||||
template_name : StringProperty()
|
||||
template_file : StringProperty()
|
||||
|
||||
url: StringProperty()
|
||||
login: StringProperty()
|
||||
password: StringProperty(subtype='PASSWORD')
|
||||
project_name: StringProperty()
|
||||
|
||||
def connect(self, url=None, login=None, password=None):
|
||||
'''Connect to kitsu api using provided url, login and password'''
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
if not self.url:
|
||||
print(f'Kitsu Url: {self.url} is empty')
|
||||
return
|
||||
|
||||
url = self.url
|
||||
if not url.endswith('/api'):
|
||||
url += '/api'
|
||||
|
||||
print(f'Info: Setting Host for kitsu {url}')
|
||||
gazu.client.set_host(url)
|
||||
|
||||
if not gazu.client.host_is_up():
|
||||
print('Error: Kitsu Host is down')
|
||||
|
||||
try:
|
||||
print(f'Info: Log in to kitsu as {self.login}')
|
||||
res = gazu.log_in(self.login, self.password)
|
||||
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
|
||||
return res['user']
|
||||
except Exception as e:
|
||||
print(f'Error: {traceback.format_exc()}')
|
||||
|
||||
def get_asset_path(self, name, catalog, directory=None):
|
||||
directory = directory or self.source_directory
|
||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||
|
||||
def get_asset_description(self, data, path):
|
||||
|
||||
modified = time.time_ns()
|
||||
catalog = data['entity_type_name']
|
||||
asset_path = Path(path)
|
||||
asset_name = self.norm_file_name(data['name'])
|
||||
|
||||
asset_description = dict(
|
||||
filepath='{source_directory}/' + asset_path.as_posix(),
|
||||
modified=modified,
|
||||
library_id=self.library.id,
|
||||
assets=[dict(
|
||||
catalog=catalog,
|
||||
metadata=data.get('data', {}),
|
||||
description=data['description'],
|
||||
tags=[],
|
||||
type=self.data_type,
|
||||
image=str(self.image_template.format(name=asset_name)),
|
||||
video=str(self.video_template.format(name=asset_name)),
|
||||
name=data['name'])
|
||||
]
|
||||
)
|
||||
|
||||
return asset_description
|
||||
|
||||
def bundle(self, cache_diff=None):
|
||||
"""Group all asset in one or multiple blends for the asset browser"""
|
||||
|
||||
return super().bundle(cache_diff=cache_diff)
|
||||
|
||||
def get_preview(self, asset_data):
|
||||
|
||||
name = asset_data['name']
|
||||
preview = (f / image_template.format(name=name)).resolve()
|
||||
if not preview.exists():
|
||||
preview_blend_file(f, preview)
|
||||
|
||||
return preview
|
||||
|
||||
def conform(self, directory, templates):
|
||||
"""Split each assets per blend and externalize preview"""
|
||||
|
||||
print(f'Conforming {self.library.name} to {directory}')
|
||||
|
||||
|
||||
def fetch(self):
|
||||
"""Gather in a list all assets found in the folder"""
|
||||
|
||||
print(f'Fetch Assets for {self.library.name}')
|
||||
|
||||
self.connect()
|
||||
|
||||
template_file = Template(self.template_file)
|
||||
template_name = Template(self.template_name)
|
||||
|
||||
project = gazu.client.fetch_first('projects', {'name': self.project_name})
|
||||
entity_types = gazu.client.fetch_all('entity-types')
|
||||
entity_types_ids = {e['id']: e['name'] for e in entity_types}
|
||||
|
||||
new_cache = []
|
||||
for asset_data in gazu.asset.all_assets_for_project(project):
|
||||
asset_data['entity_type_name'] = entity_types_ids[asset_data.pop('entity_type_id')]
|
||||
asset_name = asset_data['name']
|
||||
|
||||
asset_field_data = dict(name=asset_name, type=asset_data['entity_type_name'])
|
||||
|
||||
try:
|
||||
asset_field_data.update(template_name.parse(asset_name))
|
||||
except Exception:
|
||||
print(f'Warning: Could not parse {asset_name} with template {template_name}')
|
||||
|
||||
asset_path = template_file.find(asset_field_data)
|
||||
if not asset_path:
|
||||
print(f'Warning: Could not find file for {template_file.format(asset_field_data)}')
|
||||
continue
|
||||
|
||||
#print(asset_path)
|
||||
|
||||
new_cache.append(self.get_asset_description(asset_data, asset_path))
|
||||
|
||||
#asset = load_datablocks(asset_path, data_type='collections', names=asset_data['name'], link=True)
|
||||
#if not asset:
|
||||
# print(f"Asset {asset_name} not found in {asset_path}")
|
||||
|
||||
|
||||
#asset_description = self.get_asset_description(asset)
|
||||
|
||||
#new_cache.append(asset_description)
|
||||
|
||||
#print(assets)
|
||||
# for k, v in assets[0].items():
|
||||
# print(f'- {k} {v}')
|
||||
|
||||
return new_cache
|
||||
@@ -0,0 +1,459 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
|
||||
from asset_library.adapters.adapter import AssetLibraryAdapter
|
||||
from asset_library.common.bl_utils import load_datablocks
|
||||
from asset_library.common.template import Template
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
||||
import re
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
|
||||
|
||||
class ScanFolderLibrary(AssetLibraryAdapter):
|
||||
|
||||
name = "Scan Folder"
|
||||
source_directory : StringProperty(subtype='DIR_PATH')
|
||||
template : StringProperty()
|
||||
blend_depth : IntProperty()
|
||||
#externalize_preview : BoolProperty(default=True)
|
||||
|
||||
#def draw_header(self, layout):
|
||||
# '''Draw the header of the Asset Browser Window'''
|
||||
# layout.separator()
|
||||
# layout.operator("actionlib.store_anim_pose", text='Add Action', icon='FILE_NEW')
|
||||
|
||||
#def update(self):
|
||||
#
|
||||
def get_asset_path(self, name, catalog, directory=None):
|
||||
directory = directory or self.source_directory
|
||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||
|
||||
def get_asset_description(self, asset, catalog, modified):
|
||||
|
||||
asset_path = self.get_asset_relative_path(name=asset.name, catalog=catalog)
|
||||
asset_name = self.norm_file_name(asset.name)
|
||||
|
||||
asset_description = dict(
|
||||
filepath='{source_directory}/' + asset_path.as_posix(),
|
||||
modified=modified,
|
||||
library_id=self.library.id,
|
||||
assets=[]
|
||||
)
|
||||
|
||||
asset_description['assets'].append(dict(
|
||||
catalog=catalog,
|
||||
metadata=dict(asset.asset_data),
|
||||
tags=asset.asset_data.tags.keys(),
|
||||
type=self.data_type,
|
||||
image=str(self.image_template.format(name=asset_name)),
|
||||
video=str(self.video_template.format(name=asset_name)),
|
||||
name=asset.name)
|
||||
)
|
||||
|
||||
return asset_description
|
||||
|
||||
def _find_blend_files(self):
|
||||
'''Get a sorted list of all blender files found matching the template'''
|
||||
template = Template(self.template)
|
||||
|
||||
print(f'Search for blend using glob template: {template.glob_pattern}')
|
||||
|
||||
source_directory = Path(os.path.expandvars(self.source_directory))
|
||||
print(f'Scanning Folder {source_directory}...')
|
||||
blend_files = list(source_directory.glob(template.glob_pattern))
|
||||
|
||||
blend_files.sort()
|
||||
|
||||
return blend_files
|
||||
|
||||
def _group_key(self, asset_data):
|
||||
"""Group assets inside one blend"""
|
||||
|
||||
catalog_parts = asset_data['catalog'].split('/') + [asset_data['name']]
|
||||
|
||||
return catalog_parts[:self.blend_depth]
|
||||
|
||||
def bundle(self, cache_diff=None):
|
||||
"""Group all asset in one or multiple blends for the asset browser"""
|
||||
|
||||
if self.data_type not in ('FILE', 'ACTION'):
|
||||
print(f'{self.data_type} is not supported yet')
|
||||
return
|
||||
|
||||
lib_path = self.library_path
|
||||
catalog_data = self.read_catalog() # TODO remove unused catalog
|
||||
|
||||
#asset_file_datas = self.fetch() # TODO replace to only change new assets
|
||||
|
||||
if not cache_diff:
|
||||
# Get list of all modifications
|
||||
cache, cache_diff = self.diff()
|
||||
self.write_cache(cache)
|
||||
|
||||
elif isinstance(cache_diff, (Path, str)):
|
||||
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))
|
||||
|
||||
if self.blend_depth == 0:
|
||||
groups = [(cache_diff)]
|
||||
else:
|
||||
cache_diff.sort(key=self._group_key)
|
||||
groups = groupby(cache_diff, key=self._group_key)
|
||||
|
||||
# #print(cache_diff)
|
||||
# print('\n')
|
||||
# for sub_path, asset_datas in groups:
|
||||
|
||||
# print('\n')
|
||||
# print(f'{sub_path=}')
|
||||
# print(f'asset_datas={list(asset_datas)}')
|
||||
|
||||
# raise Exception()
|
||||
|
||||
#progress = 0
|
||||
total_assets = len(cache_diff)
|
||||
print(f'total_assets={total_assets}')
|
||||
|
||||
if total_assets == 0:
|
||||
print('No assets found')
|
||||
return
|
||||
|
||||
i = 0
|
||||
for sub_path, asset_datas in groups:
|
||||
|
||||
# print('\n')
|
||||
# print(f'{sub_path=}')
|
||||
# print(f'asset_datas={list(asset_datas)}')
|
||||
|
||||
# print('\n')
|
||||
|
||||
blend_name = sub_path[-1].replace(' ', '_').lower()
|
||||
blend_path = Path(lib_path, *sub_path, blend_name).with_suffix('.blend')
|
||||
|
||||
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_data in asset_datas:
|
||||
if total_assets <= 100 or i % int(total_assets / 10) == 0:
|
||||
print(f'Progress: {int(i / total_assets * 100)+1}')
|
||||
|
||||
operation = asset_data.get('operation', 'ADD')
|
||||
asset = getattr(bpy.data, self.data_types).get(asset_data['name'])
|
||||
|
||||
if operation == 'REMOVE':
|
||||
if asset:
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
else:
|
||||
print(f'ERROR : Remove Asset: {asset_data["name"]} not found in {blend_path}')
|
||||
continue
|
||||
|
||||
elif operation == 'MODIFY':
|
||||
if not asset:
|
||||
print(f'WARNING: Modifiy Asset: {asset_data["name"]} not found in {blend_path} it will be created')
|
||||
|
||||
elif operation == 'ADD' or not asset:
|
||||
if asset:
|
||||
#raise Exception(f"Asset {asset_data['name']} Already in Blend")
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
#print(f"INFO: Add new asset: {asset_data['name']}")
|
||||
asset = getattr(bpy.data, self.data_types).new(name=asset_data['name'])
|
||||
else:
|
||||
print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFIED)')
|
||||
continue
|
||||
|
||||
asset.asset_mark()
|
||||
|
||||
# Load external preview if exists
|
||||
#image_template = Template(asset_data['preview'])
|
||||
image_path = Path(asset_data['image'])
|
||||
if not image_path.is_absolute():
|
||||
image_path = Path(asset_data['filepath'], image_path)
|
||||
|
||||
image_path = self.format_path(image_path.as_posix())
|
||||
|
||||
if image_path and image_path.exists():
|
||||
with bpy.context.temp_override(id=asset):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=str(image_path)
|
||||
)
|
||||
#else:
|
||||
# print(f'Preview {image_path} not found for asset {asset}')
|
||||
|
||||
asset.asset_data.description = asset_data.get('description', '')
|
||||
|
||||
catalog_name = asset_data['catalog']
|
||||
catalog = catalog_data.get(catalog_name)
|
||||
if not catalog:
|
||||
catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
||||
catalog_data[catalog_name] = catalog
|
||||
|
||||
asset.asset_data.catalog_id = catalog['id']
|
||||
|
||||
metadata = asset_data.get('metadata', {})
|
||||
|
||||
library_id = self.library.id
|
||||
if 'library_id' in asset_data:
|
||||
library_id = asset_data['library_id']
|
||||
|
||||
metadata['.library_id'] = library_id
|
||||
|
||||
#print(metadata)
|
||||
|
||||
metadata['filepath'] = asset_data['filepath']
|
||||
for k, v in metadata.items():
|
||||
asset.asset_data[k] = v
|
||||
|
||||
# Set tags if specified the asset_description
|
||||
tags = asset_data.get('tags', [])
|
||||
if tags:
|
||||
for tag in asset.asset_data.tags[:]:
|
||||
asset.asset_data.tags.remove(tag)
|
||||
|
||||
for tag in tags:
|
||||
if not tag:
|
||||
continue
|
||||
asset.asset_data.tags.new(tag, skip_if_exists=True)
|
||||
|
||||
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)
|
||||
|
||||
self.write_catalog(catalog_data)
|
||||
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
def get_preview(self, asset_data):
|
||||
|
||||
name = asset_data['name']
|
||||
preview = (f / image_template.format(name=name)).resolve()
|
||||
if not preview.exists():
|
||||
preview_blend_file(f, preview)
|
||||
|
||||
return preview
|
||||
|
||||
|
||||
def conform(self, directory, templates):
|
||||
"""Split each assets per blend and externalize preview"""
|
||||
|
||||
print(f'Conforming {self.library.name} to {directory}')
|
||||
|
||||
if self.data_type not in ('FILE', 'ACTION'):
|
||||
print(f'{self.data_type} is not supported yet')
|
||||
return
|
||||
|
||||
#lib_path = self.library_path
|
||||
source_directory = Path(os.path.expandvars(self.source_directory))
|
||||
catalog_data = self.read_catalog(filepath=source_directory)
|
||||
catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_data.items()}
|
||||
directory = Path(directory).resolve()
|
||||
|
||||
image_template = templates.get('image') or self.image_template
|
||||
video_template = templates.get('video') or self.video_template
|
||||
|
||||
# Get list of all modifications
|
||||
for blend_file in self._find_blend_files():
|
||||
|
||||
modified = blend_file.stat().st_mtime_ns
|
||||
|
||||
print(f'Scanning blendfile {blend_file}...')
|
||||
with bpy.data.libraries.load(str(blend_file), link=True, assets_only=True) as (data_from, data_to):
|
||||
asset_names = getattr(data_from, self.data_types)
|
||||
print(f'Found {len(asset_names)} {self.data_types} inside')
|
||||
|
||||
setattr(data_to, self.data_types, asset_names)
|
||||
|
||||
assets = getattr(data_to, self.data_types)
|
||||
#print('assets', assets)
|
||||
|
||||
for asset in assets:
|
||||
#TODO options for choose beetween asset catalog and filepath directory
|
||||
asset_catalog_data = catalog_ids.get(asset.asset_data.catalog_id)
|
||||
|
||||
if not asset_catalog_data:
|
||||
print(f'No catalog found for asset {asset.name}')
|
||||
asset_catalog_data = {"path": blend_file.parent.relative_to(source_directory).as_posix()}
|
||||
|
||||
catalog_path = asset_catalog_data['path']
|
||||
|
||||
asset_path = self.get_asset_path(name=asset.name, catalog=catalog_path, directory=directory)
|
||||
asset_description = self.get_asset_description(asset, catalog=catalog_path, modified=modified)
|
||||
|
||||
self.write_asset_description(asset_description, asset_path)
|
||||
#Write blend file containing only one asset
|
||||
self.write_asset(asset=asset, asset_path=asset_path)
|
||||
|
||||
# Copy image if source image found else write the asset preview
|
||||
src_image_path = self.get_path('image', name=asset.name, asset_path=blend_file, template=image_template)
|
||||
dst_image_path = self.get_path('image', name=asset.name, asset_path=asset_path)
|
||||
|
||||
if src_image_path.exists():
|
||||
self.copy_file(src_image_path, dst_image_path)
|
||||
else:
|
||||
self.write_preview(asset.preview, dst_image_path)
|
||||
|
||||
# Copy video if source video found
|
||||
src_video_path = self.get_path('video', name=asset.name, asset_path=blend_file, template=video_template)
|
||||
|
||||
#print('src_video_path', src_video_path)
|
||||
if src_video_path.exists():
|
||||
dst_video_path = self.get_path('video', name=asset.name, asset_path=asset_path)
|
||||
self.copy_file(src_video_path, dst_video_path)
|
||||
|
||||
self.write_catalog(catalog_data, filepath=directory)
|
||||
|
||||
def fetch(self):
|
||||
"""Gather in a list all assets found in the folder"""
|
||||
|
||||
print(f'Fetch Assets for {self.library.name}')
|
||||
|
||||
source_directory = Path(os.path.expandvars(self.source_directory))
|
||||
template = Template(self.template)
|
||||
catalog_data = self.read_catalog(filepath=source_directory)
|
||||
catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_data.items()}
|
||||
|
||||
cache = self.read_cache() or []
|
||||
|
||||
print(f'Search for blend using glob template: {template.glob_pattern}')
|
||||
|
||||
print(f'Scanning Folder {source_directory}...')
|
||||
#blend_files = list(source_directory.glob(template.glob_pattern))
|
||||
|
||||
# Remove delete blends for the list
|
||||
#blend_paths = [self.prop_rel_path(f, 'source_directory') for f in blend_files]
|
||||
#print('blend_paths', blend_paths)
|
||||
|
||||
|
||||
#cache = []
|
||||
#blend_paths = []
|
||||
new_cache = []
|
||||
|
||||
for blend_file in template.glob(source_directory):#sorted(blend_files):
|
||||
|
||||
source_rel_path = self.prop_rel_path(blend_file, 'source_directory')
|
||||
modified = blend_file.stat().st_mtime_ns
|
||||
|
||||
asset_description = next((a for a in cache if a['filepath'] == source_rel_path), None)
|
||||
|
||||
if asset_description and asset_description['modified'] >= modified:
|
||||
print(blend_file, 'is skipped because not modified')
|
||||
new_cache.append(asset_description)
|
||||
continue
|
||||
|
||||
rel_path = blend_file.relative_to(source_directory).as_posix()
|
||||
#field_values = re.findall(re_pattern, rel_path)[0]
|
||||
#field_data = {k:v for k,v in zip(field_names, field_values)}
|
||||
field_data = template.parse(rel_path)
|
||||
|
||||
if not field_data:
|
||||
raise Exception()
|
||||
|
||||
#asset_data = (blend_file / prefs.asset_description_template.format(name=name)).resolve()
|
||||
|
||||
catalogs = [v for k,v in sorted(field_data.items()) if k.isdigit()]
|
||||
catalogs = [c.replace('_', ' ').title() for c in catalogs]
|
||||
|
||||
if self.data_type == 'FILE':
|
||||
name = field_data.get('name', blend_file.stem)
|
||||
image = self.get_path('image', name=name, asset_path=blend_file)
|
||||
|
||||
asset_description = dict(
|
||||
filepath=source_rel_path,
|
||||
modified=modified,
|
||||
catalog='/'.join(catalogs),
|
||||
tags=[],
|
||||
type=self.data_type,
|
||||
image=self.prop_rel_path(image, 'source_directory'),
|
||||
name=name
|
||||
)
|
||||
new_cache.append(asset_description)
|
||||
|
||||
continue
|
||||
|
||||
#First Check if there is a asset_data .json
|
||||
asset_description = self.read_asset_description(blend_file)
|
||||
|
||||
if not asset_description:
|
||||
# Scan the blend file for assets inside and write a custom asset description for info found
|
||||
|
||||
print(f'Scanning blendfile {blend_file}...')
|
||||
with bpy.data.libraries.load(str(blend_file), link=True, assets_only=True) as (data_from, data_to):
|
||||
asset_names = getattr(data_from, self.data_types)
|
||||
print(f'Found {len(asset_names)} {self.data_types} inside')
|
||||
|
||||
setattr(data_to, self.data_types, asset_names)
|
||||
assets = getattr(data_to, self.data_types)
|
||||
|
||||
asset_description = dict(
|
||||
filepath=source_rel_path,
|
||||
modified=modified,
|
||||
assets=[]
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
asset_catalog_data = catalog_ids.get(asset.asset_data.catalog_id)
|
||||
|
||||
if not asset_catalog_data:
|
||||
print(f'No catalog found for asset {asset.name}')
|
||||
asset_catalog_data = {"path": blend_file.relative_to(self.source_directory).as_posix()}
|
||||
|
||||
catalog_path = asset_catalog_data['path']
|
||||
|
||||
image_path = self.get_path('image', asset.name, catalog_path)
|
||||
image = self.prop_rel_path(image_path, 'source_directory')
|
||||
|
||||
# Write image only if no image was found
|
||||
if not image_path.exists():
|
||||
image_path = self.get_cache_image_path(asset.name, catalog_path)
|
||||
image = self.prop_rel_path(image_path, 'library_path')
|
||||
self.write_preview(asset.preview, image_path)
|
||||
|
||||
video_path = self.get_path('video', asset.name, catalog_path)
|
||||
video = self.prop_rel_path(video_path, 'source_directory')
|
||||
|
||||
asset_data = dict(
|
||||
filepath=self.prop_rel_path(blend_file, 'source_directory'),
|
||||
modified=modified,
|
||||
catalog=catalog_path,
|
||||
tags=asset.asset_data.tags.keys(),
|
||||
type=self.data_type,
|
||||
image=image,
|
||||
video=video,
|
||||
name=asset.name
|
||||
)
|
||||
asset_description['assets'].append(asset_data)
|
||||
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
new_cache.append(asset_description)
|
||||
|
||||
|
||||
#cache = [a for a in cache if a['filepath'] in blend_paths]
|
||||
|
||||
#for a in asset_data:
|
||||
# print(a)
|
||||
|
||||
#print(asset_data)
|
||||
new_cache.sort(key=lambda x:x['filepath'])
|
||||
|
||||
return new_cache
|
||||
|
||||
# Write json data file to store all asset found
|
||||
#print(f'Writing asset data file to, {asset_data_path}')
|
||||
#asset_data_path.write_text(json.dumps(asset_data, indent=4))
|
||||
Reference in New Issue
Block a user