asset_library/plugins/scan_folder.py

303 lines
11 KiB
Python

"""
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