Files
at-mintel/blender-mv.sh
Marc Mintel 13c8bc29ae
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m43s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m14s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s
chore: add blender-mv utility
2026-04-21 18:47:53 +02:00

178 lines
5.6 KiB
Bash
Executable File

#!/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 <dir> 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."