diff --git a/blender-mv.sh b/blender-mv.sh new file mode 100755 index 0000000..650a5ec --- /dev/null +++ b/blender-mv.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# blender-mv: A smart file/directory mover that automatically updates Blender paths + +set -e + +print_usage() { + echo "Usage: blender-mv [OPTIONS] SOURCE DEST" + echo "" + echo "Options:" + echo " --search-project Scan this specific project directory for .blend files" + echo " to update (useful if you move an external asset folder," + echo " and old .blend files referencing it are elsewhere)." + echo "" + echo "Example: blender-mv ./old_folder ./new_folder" +} + +SEARCH_PROJECT="" + +# Parse arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --search-project) SEARCH_PROJECT="$2"; shift 2 ;; + -h|--help) print_usage; exit 0 ;; + *) + if [[ -z "$SOURCE" ]]; then + SOURCE="$1" + elif [[ -z "$DEST" ]]; then + DEST="$1" + else + echo "Unknown parameter passed: $1" + print_usage + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$SOURCE" || -z "$DEST" ]]; then + echo "Error: Both SOURCE and DEST must be provided." + print_usage + exit 1 +fi + +if [[ ! -e "$SOURCE" ]]; then + echo "Error: Source path '$SOURCE' does not exist." + exit 1 +fi + +# Function to get absolute normalized path (handles missing destination gracefully) +get_abs_path() { + python3 -c 'import os,sys; print(os.path.normpath(os.path.abspath(sys.argv[1])))' "$1" 2>/dev/null +} + +# Resolve source before moving +ABS_SOURCE=$(get_abs_path "$SOURCE") + +# Determine resulting target path +if [[ -d "$DEST" ]]; then + # If DEST is an existing dir, standard mv puts SOURCE inside it + BASENAME_SOURCE=$(basename "$SOURCE") + ABS_DEST=$(get_abs_path "$DEST/$BASENAME_SOURCE") +else + # Placed exactly at DEST (rename or target dir doesn't exist yet) + ABS_DEST=$(get_abs_path "$DEST") +fi + +echo "Moving: $SOURCE -> $DEST" +mv "$SOURCE" "$DEST" + +echo "Replacing paths in Blender's recent-files lists..." +# Find all recent-files.txt across Blender versions and replace the string +find "$HOME/Library/Application Support/Blender" -type f -name "recent-files.txt" 2>/dev/null | while read -r RECENT_FILE; do + # Using sed with alternative delimiter to avoid path interference + sed -i '' "s|${ABS_SOURCE}|${ABS_DEST}|g" "$RECENT_FILE" +done + +# Prepare the embedded Python payload to update blend paths +TMP_PY=$(mktemp /tmp/blender_update_paths.XXXXXX.py) +cat << 'EOF' > "$TMP_PY" +import bpy +import sys + +def replace_in_prop(prop, old_path, new_path): + if prop and isinstance(prop, str) and old_path in prop: + return prop.replace(old_path, new_path) + return prop + +def run(): + # Args are passed after `--` + if '--' not in sys.argv: + return + idx = sys.argv.index('--') + if len(sys.argv) <= idx + 2: + return + + old_path = sys.argv[idx + 1] + new_path = sys.argv[idx + 2] + + changed = False + + # 1. Libraries (Linked objects/collections) + for lib in bpy.data.libraries: + new_fp = replace_in_prop(lib.filepath, old_path, new_path) + if new_fp != lib.filepath: + lib.filepath = new_fp + changed = True + + # 2. Images (Textures, Env Maps) + for img in bpy.data.images: + new_fp = replace_in_prop(img.filepath, old_path, new_path) + if new_fp != img.filepath: + img.filepath = new_fp + changed = True + + # 3. Cache files (Alembic/VDB) + for cache in bpy.data.cache_files: + new_fp = replace_in_prop(cache.filepath, old_path, new_path) + if new_fp != cache.filepath: + cache.filepath = new_fp + changed = True + + # 4. Fonts + for font in bpy.data.fonts: + new_fp = replace_in_prop(font.filepath, old_path, new_path) + if new_fp != font.filepath: + font.filepath = new_fp + changed = True + + # 5. Video strips + for scene in bpy.data.scenes: + if scene.sequence_editor: + for strip in scene.sequence_editor.sequences_all: + if strip.type in ('MOVIE', 'IMAGE'): + new_fp = replace_in_prop(strip.filepath, old_path, new_path) + if new_fp != strip.filepath: + strip.filepath = new_fp + changed = True + + if changed: + print(f"[*] Updated paths in {bpy.data.filepath}") + bpy.ops.wm.save_mainfile() + +try: + run() +except Exception as e: + print(f"[!] Error updating blend file paths: {e}") +EOF + +# Determine what to search for blend files +if [[ -n "$SEARCH_PROJECT" ]]; then + SEARCH_ROOT=$(get_abs_path "$SEARCH_PROJECT") + echo "Scanning project root '$SEARCH_ROOT' for .blend files..." +elif [[ -d "$ABS_DEST" ]]; then + SEARCH_ROOT="$ABS_DEST" + echo "Scanning moved directory for .blend files..." +elif [[ "$ABS_DEST" == *.blend ]]; then + SEARCH_ROOT="$ABS_DEST" + echo "Updating moved .blend file..." +else + echo "No project root defined and moved object is not a directory or .blend file." + echo "Finished." + rm -f "$TMP_PY" + exit 0 +fi + +# Execute headless blender on all found blend files +if [[ -d "$SEARCH_ROOT" ]]; then + find "$SEARCH_ROOT" -type f -name "*.blend" -print0 | while IFS= read -r -d '' BLEND_FILE; do + blender -b "$BLEND_FILE" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $BLEND_FILE" + done +elif [[ -f "$SEARCH_ROOT" && "$SEARCH_ROOT" == *.blend ]]; then + blender -b "$SEARCH_ROOT" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $SEARCH_ROOT" +fi + +rm -f "$TMP_PY" +echo "Success: Move complete and paths updated."