import bpy import os import shutil def get_external_datablocks(): """Return a list of external data-blocks""" out = [] for attr in dir(bpy.data): collections = getattr(bpy.data, attr) if isinstance(collections, type(bpy.data.objects)): for data_block in collections: if (isinstance(data_block, bpy.types.Image) or isinstance(data_block, bpy.types.MovieClip)) and hasattr(data_block, "filepath"): if hasattr(data_block, "packed_file"): print(f"[{'P' if data_block.packed_file is not None else ' '}] [{data_block.__class__}] {data_block.source}: {data_block.filepath}") else: print(f" [{data_block.__class__}] {data_block.source}: {data_block.filepath}") out.append(data_block) return out def collect_data(data_block, dirpath, sequence_in_subfolder=True): """Copy data into a given folder. Only work for 'MOVIE' and 'SEQUENCE' data-blocks""" if data_block.source == "MOVIE": source_path = os.path.normpath(bpy.path.abspath(data_block.filepath)) dest_path = os.path.join(dirpath, os.path.basename(source_path)) print(f"Copying FROM '{source_path}' TO '{dest_path}'") shutil.copyfile(source_path, dest_path) elif data_block.source == "SEQUENCE": source_dirpath = os.path.dirname(os.path.normpath(bpy.path.abspath(data_block.filepath))) if sequence_in_subfolder: dest_dirpath = os.path.join(dirpath, os.path.basename(source_dirpath)) else: dest_dirpath = dirpath if not os.path.isdir(dest_dirpath): os.mkdir(dest_dirpath) print(f"Copying FROM '{source_dirpath}' TO '{dest_dirpath}'") for filename in os.listdir(source_dirpath): if os.path.splitext(filename)[1] != ".db": shutil.copyfile(os.path.join(source_dirpath, filename), os.path.join(dest_dirpath, filename)) def get_source_datablocks(datablocks, source): """Return the list data-blocks which are of specific sources""" out = [] for data_block in datablocks: if data_block.source == source: out.append(data_block) return out def main(): external_datablocks = get_external_datablocks() print(f"{len(external_datablocks)} external data-blocks") bl_dirpath = os.path.dirname(bpy.data.filepath) bl_filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] if len(external_datablocks) > 0: # Create a subfolder per blend file external_data_dirpath = os.path.join(bl_dirpath, f"{bl_filename}-external_data") if not os.path.isdir(external_data_dirpath): os.mkdir(external_data_dirpath) for data_block in external_datablocks: collect_data(data_block, external_data_dirpath) class WM_OT_ConsolidateProject(bpy.types.Operator): """Open the consolidation dialog box""" bl_label = "Consolidate Project" bl_idname = "wm.consolidate_project" my_purge_unused_data: bpy.props.BoolProperty(name="Purge Unused Data", default=True) my_pack_ressources: bpy.props.BoolProperty(name="Pack External Ressources", default=True) my_collect_movies: bpy.props.BoolProperty(name="Collect Movies", default=True) my_collect_sequences: bpy.props.BoolProperty(name="Collect Image Sequences", default=True) my_sequences_in_subfolders: bpy.props.BoolProperty(name="Save Image Sequences in Sub-Folders", default=False) my_external_datablocks = [] my_external_movies = [] my_external_sequences = [] my_dirpath = None my_filename = None my_external_data_dirpath = None def execute(self, context): if self.my_purge_unused_data: bpy.ops.outliner.orphans_purge() if self.my_pack_ressources: bpy.ops.file.pack_all() if self.my_collect_movies: for data_block in self.my_external_movies: collect_data(data_block, self.my_external_data_dirpath) if self.my_collect_sequences: for data_block in self.my_external_sequences: collect_data(data_block, self.my_external_data_dirpath, self.my_sequences_in_subfolders) self.report({'INFO'}, "Consolidation complete") return {"FINISHED"} def invoke(self, context, event): self.my_external_datablocks = get_external_datablocks() self.my_external_movies = get_source_datablocks(self.my_external_datablocks, source='MOVIE') self.my_external_sequences = get_source_datablocks(self.my_external_datablocks, source='SEQUENCE') self.my_dirpath = os.path.dirname(bpy.data.filepath) self.my_filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] self.my_external_data_dirpath = os.path.join(self.my_dirpath, f"{self.my_filename}-external_data") if not os.path.isdir(self.my_external_data_dirpath): os.mkdir(self.my_external_data_dirpath) return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout col = layout.column() col.label(text="Cleaning and Packing Ressources", icon="PACKAGE") row = col.row() row.prop(self, "my_purge_unused_data") row = col.row() row.prop(self, "my_pack_ressources") layout.separator() col = layout.column() col.label(text="Collect movies and image sequences", icon="FILE_MOVIE") row = col.row() row.prop(self, "my_collect_movies") row = col.row() row.prop(self, "my_collect_sequences") row = col.row() row.prop(self, "my_sequences_in_subfolders", icon="NEWFOLDER") row.enabled = self.my_collect_sequences box = layout.box() box.label(text="External data-blocks") for data_block in self.my_external_datablocks: row = box.row() display_data_block(data_block, row) def display_data_block(data_block, layout): split = layout.split(factor=.9) if data_block.source in ["SEQUENCE", "MOVIE"]: split.label(text=f"{data_block.name}", icon="FILE_MOVIE") #split.label(text="", icon="WARNING_LARGE") else: split.label(text=f"{data_block.name}", icon="FILE_IMAGE") if hasattr(data_block, "packed_file"): if data_block.packed_file is not None: split.label(text=f"", icon="PACKAGE") else: split.label(text=f"", icon="UGLYPACKAGE") def menu_func_import(self, context): self.layout.operator(WM_OT_ConsolidateProject.bl_idname, text="ETNCL - Consolidate", icon="PACKAGE") self.layout.separator() blender_classes = [ WM_OT_ConsolidateProject ] def register(): for cls in blender_classes: bpy.utils.register_class(cls) bpy.types.TOPBAR_MT_file_external_data.prepend(menu_func_import) def unregister(): for cls in blender_classes: bpy.utils.unregister_class(cls) bpy.types.TOPBAR_MT_file_external_data.remove(menu_func_import) if __name__ == "__main__": register()