├── .gitignore ├── README.md └── manage_file_paths.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manage File Paths 2 | 3 | A blender add-on thrown together in a hurry that's supposed to help manage... file paths... yep. 4 | 5 | In very early stages still, just has some basic functionality. 6 | 7 | Supports images and caches. 8 | 9 | ## Installation 10 | 11 | [Download the .py file](https://raw.githubusercontent.com/gregzaal/manage-file-paths/master/manage_file_paths.py) (right click > Save As) and install it in your Blender preferences. 12 | -------------------------------------------------------------------------------- /manage_file_paths.py: -------------------------------------------------------------------------------- 1 | # BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Manage File Paths", 21 | "description": "Lists file paths for images and caches, showing broken paths, and allowing Find & Replace in paths.", 22 | "author": "Greg Zaal", 23 | "version": (0, 3), 24 | "blender": (2, 91, 0), 25 | "location": "Properties Editor > Scene > File Paths panel", 26 | "warning": "", 27 | "wiki_url": "https://github.com/gregzaal/manage-file-paths", 28 | "tracker_url": "https://github.com/gregzaal/manage-file-paths/issues", 29 | "category": "Scene"} 30 | 31 | import bpy 32 | import os 33 | from shutil import copy2 as copyfile 34 | 35 | ''' 36 | TODOs: 37 | Don't fetch image list or check for existance on every redraw, make refresh button (and maybe refresh automatically every so often with modal timer?) 38 | Find and replace individually 39 | Only if file doesn't exist 40 | Somehow automatically try fix paths based on history (stored in config) 41 | Reload images 42 | ''' 43 | 44 | 45 | class MFPProps(bpy.types.PropertyGroup): 46 | bl_idname = __package__ 47 | 48 | source: bpy.props.StringProperty( 49 | name="Find", 50 | default="", 51 | description="source") 52 | 53 | target: bpy.props.StringProperty( 54 | name="Replace", 55 | default="", 56 | description="target") 57 | 58 | 59 | def get_images(): 60 | images = [] 61 | for i in bpy.data.images: 62 | if i.source == 'FILE': 63 | if not i.library: # ignore linked images since we cannot modify them 64 | images.append(i) 65 | return images 66 | 67 | 68 | def file_exists(path): 69 | if path.startswith('//'): # os.path.exists only works with absolute paths 70 | path = bpy.path.abspath(path) 71 | 72 | return os.path.exists(path) 73 | 74 | 75 | def all_rel_to_abs(): 76 | images = get_images() 77 | for img in images: 78 | if img.filepath.startswith('//'): 79 | img.filepath = bpy.path.abspath(img.filepath) 80 | 81 | 82 | class MFP_OT_FindReplace(bpy.types.Operator): 83 | """Replace the text specified in 'Find' with that in 'Replace'""" 84 | bl_idname = "mfp.find_replace" 85 | bl_label = "Find & Replace" 86 | bl_options = {'REGISTER', 'UNDO'} 87 | 88 | def execute(self, context): 89 | props = context.scene.mfp_props 90 | images = get_images() 91 | 92 | for img in images: 93 | img.filepath = img.filepath.replace(props.source, props.target) 94 | return {'FINISHED'} 95 | 96 | 97 | class MFP_OT_Copy(bpy.types.Operator): 98 | """Tooltip""" # TODO 99 | bl_idname = "mfp.copy" 100 | bl_label = "Copy Source to Target" 101 | 102 | def execute(self, context): 103 | all_rel_to_abs() 104 | props = context.scene.mfp_props 105 | images = get_images() 106 | 107 | for img in images: 108 | old_path = img.filepath 109 | if props.source in old_path: 110 | new_path = old_path.replace(props.source, props.target) 111 | new_path_root = os.sep.join(new_path.split(os.sep)[:-1]) 112 | if not os.path.exists(new_path_root): 113 | os.makedirs(new_path_root) 114 | img.filepath = new_path 115 | copyfile (old_path, new_path) 116 | return {'FINISHED'} 117 | 118 | 119 | class MFP_PT_ImagePathsPanel(bpy.types.Panel): 120 | bl_label = "File Paths" 121 | bl_space_type = 'PROPERTIES' 122 | bl_region_type = 'WINDOW' 123 | bl_context = "scene" 124 | 125 | def draw(self, context): 126 | layout = self.layout 127 | images = get_images() 128 | caches = bpy.data.cache_files 129 | props = context.scene.mfp_props 130 | 131 | col = layout.column() 132 | for img in images: 133 | row = col.row(align=True) 134 | row.prop(img, 'filepath', text=img.name) 135 | i = 'BLANK1' if file_exists(img.filepath) else 'LIBRARY_DATA_BROKEN' 136 | row.label(text='', icon=i) 137 | 138 | if caches: 139 | col = layout.column() 140 | for c in caches: 141 | row = col.row(align=True) 142 | row.prop(c, 'filepath', text=c.name) 143 | i = 'BLANK1' if file_exists(c.filepath) else 'LIBRARY_DATA_BROKEN' 144 | row.label(text='', icon=i) 145 | 146 | col = layout.column(align=True) 147 | col.operator('file.find_missing_files') 148 | col.operator('file.make_paths_relative') 149 | col.operator('outliner.orphans_purge', text="Remove Unused") 150 | 151 | 152 | class MFP_PT_FindReplace(bpy.types.Panel): 153 | bl_label = "Find & Replace" 154 | bl_space_type = 'PROPERTIES' 155 | bl_region_type = 'WINDOW' 156 | bl_context = "scene" 157 | bl_parent_id = "MFP_PT_ImagePathsPanel" 158 | 159 | def draw(self, context): 160 | layout = self.layout 161 | props = context.scene.mfp_props 162 | 163 | col = layout.column(align=True) 164 | col.prop(props, 'source') 165 | col.prop(props, 'target') 166 | col.separator() 167 | col.operator('mfp.find_replace') 168 | 169 | 170 | classes = [ 171 | MFPProps, 172 | MFP_OT_FindReplace, 173 | MFP_PT_ImagePathsPanel, 174 | MFP_PT_FindReplace, 175 | ] 176 | 177 | 178 | def register(): 179 | from bpy.utils import register_class 180 | for cls in classes: 181 | register_class(cls) 182 | 183 | bpy.types.Scene.mfp_props = bpy.props.PointerProperty(type=MFPProps) 184 | 185 | 186 | def unregister(): 187 | del bpy.types.Scene.mfp_props 188 | 189 | from bpy.utils import unregister_class 190 | for cls in reversed(classes): 191 | unregister_class(cls) 192 | 193 | 194 | if __name__ == "__main__": 195 | register() 196 | --------------------------------------------------------------------------------