From 362872971624ce66915c20ba1118d2f26fbc4789 Mon Sep 17 00:00:00 2001 From: Raincloud Date: Fri, 27 Jun 2025 09:01:51 -0600 Subject: [PATCH] my changes so far --- .gitignore | 4 + autoThumbs.bat | 26 ++++++ nascleanup.sh | 8 ++ psthumbgen.py | 178 +++++++++++++++++++++++++++++++++++++--- run_thumbgen_prompt.bat | 8 ++ 5 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 autoThumbs.bat create mode 100644 nascleanup.sh create mode 100644 run_thumbgen_prompt.bat diff --git a/.gitignore b/.gitignore index 4e0bbdf..824462f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ venv/ .idea/ + +specstory/ +*.specstory +*.cursorindexingignore \ No newline at end of file diff --git a/autoThumbs.bat b/autoThumbs.bat new file mode 100644 index 0000000..c3426a7 --- /dev/null +++ b/autoThumbs.bat @@ -0,0 +1,26 @@ +@echo off +setlocal + +REM Always use 'nathan' as NAS username +set NASUSER=nathan + +REM Prompt for Windows path +set /p WINPATH=Enter the full Windows path to your NAS directory (e.g., R:\YouTube\Streams\MixerTwitch): +if "%WINPATH%"=="" ( + echo No path provided. Exiting. + exit /b +) + +REM Run psthumbgen.py +python psthumbgen.py --directory "%WINPATH%" + +REM Convert Windows path to NAS path +set "RELPATH=%WINPATH:~3%" +set "RELPATH=%RELPATH:\=/%" +set "NASPATH=/volume1/Hydra/%RELPATH%" + +REM SSH cleanup commands (run separately) +ssh %NASUSER%@hydra "find '%NASPATH%' -type d -name '@eaDir' -exec rm -rf '{}' \;" +ssh %NASUSER%@hydra "find '%NASPATH%' -depth -type d -name 'eaDir_tmp' -exec bash -c 'mv \"\$1\" \"\$(dirname \"\$1\")/@eaDir\"' _ {} \;" + +pause \ No newline at end of file diff --git a/nascleanup.sh b/nascleanup.sh new file mode 100644 index 0000000..0794db8 --- /dev/null +++ b/nascleanup.sh @@ -0,0 +1,8 @@ +# 1. Dry-run check (see what @eaDir directories exist) +find /volume1/Hydra/Creative/artsy -type d -name '@eaDir' -exec echo '{}' \; + +# 2. Remove any existing @eaDir directories +find /volume1/Hydra/Creative/artsy -type d -name '@eaDir' -exec rm -rf '{}' \; + +# 3. Rename eaDir_tmp to @eaDir (corrected syntax) +find /volume1/Hydra/Creative/artsy -type d -name 'eaDir_tmp' -exec bash -c 'mv "$0" "${0%/*}/@eaDir"' {} \; \ No newline at end of file diff --git a/psthumbgen.py b/psthumbgen.py index d009545..9a11a21 100644 --- a/psthumbgen.py +++ b/psthumbgen.py @@ -4,9 +4,13 @@ import re import argparse import errno import time +import subprocess from PIL import Image from multiprocessing import Pool from multiprocessing import Value +from PIL import Image, ImageDraw, ImageFont +import io +import struct class State(object): @@ -55,14 +59,18 @@ def parse_args(): def find_files(dir): - valid_exts = ('jpeg', 'jpg', 'bmp', 'gif') + # Only process formats that Synology doesn't handle well + # Exclude common images (jpg, png, gif, etc.) since NAS handles them fine + valid_exts = ('mp4', 'webm', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'm4v', 'ts', + 'psd', 'blend') valid_exts_re = "|".join( map((lambda ext: ".*\\.{0}$".format(ext)), valid_exts)) for root, dirs, files in os.walk(dir): for name in files: if re.match(valid_exts_re, name, re.IGNORECASE) \ - and not name.startswith('SYNOPHOTO_THUMB'): + and not name.startswith('SYNOPHOTO_THUMB') \ + and not name.startswith('SYNOVIDEO_VIDEO_SCREENSHOT'): yield os.path.join(root, name) @@ -71,9 +79,10 @@ def print_progress(): state.increment(1) processed = state.value if processed % 10 == 0: + elapsed = float(time.process_time() - state.start) + rate = float(processed) / elapsed if elapsed > 0 else float(processed) print("{0} files processed so far, averaging {1:.2f} files per second." - .format(processed, float(processed) / - (float(time.process_time() - state.start)))) + .format(processed, rate)) def process_file(file_path): @@ -83,7 +92,18 @@ def process_file(file_path): thumb_dir = os.path.join(dir, 'eaDir_tmp', filename) ensure_directory_exists(thumb_dir) - create_thumbnails(file_path, thumb_dir) + # Determine file type and process accordingly + file_ext = os.path.splitext(filename)[1].lower() + + if file_ext in ['.mp4', '.webm', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.m4v', '.ts']: + create_video_thumbnails(file_path, thumb_dir) + elif file_ext in ['.psd']: + create_psd_thumbnails(file_path, thumb_dir) + elif file_ext in ['.blend']: + create_blend_thumbnails(file_path, thumb_dir) + else: + # Standard image processing + create_thumbnails(file_path, thumb_dir) print_progress() @@ -96,8 +116,118 @@ def ensure_directory_exists(path): raise -def create_thumbnails(source_path, dest_dir): - im = Image.open(source_path) +def create_video_thumbnails(source_path, dest_dir): + """Generate video thumbnails using FFmpeg""" + to_generate = (('SYNOVIDEO_VIDEO_SCREENSHOT.jpg', 1280), + ('SYNOPHOTO_THUMB_XL.jpg', 1280), + ('SYNOPHOTO_THUMB_B.jpg', 640), + ('SYNOPHOTO_THUMB_M.jpg', 320), + ('SYNOPHOTO_THUMB_PREVIEW.jpg', 160), + ('SYNOPHOTO_THUMB_S.jpg', 120)) + + for thumb in to_generate: + thumb_path = os.path.join(dest_dir, thumb[0]) + if os.path.exists(thumb_path): + continue + + try: + # Use FFmpeg to extract a frame from the video at 10% duration + cmd = [ + 'ffmpeg', '-i', source_path, + '-ss', '00:00:05', # Seek to 5 seconds + '-vframes', '1', + '-vf', f'scale={thumb[1]}:{thumb[1]}:force_original_aspect_ratio=decrease', + '-y', # Overwrite output file + thumb_path + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"Warning: FFmpeg failed for {source_path}: {result.stderr}") + continue + + except FileNotFoundError: + print("Warning: FFmpeg not found. Video thumbnails will be skipped.") + print("Install FFmpeg: https://ffmpeg.org/download.html") + break + except Exception as e: + print(f"Error processing video {source_path}: {e}") + + +def create_psd_thumbnails(source_path, dest_dir): + """Generate PSD thumbnails using PIL with PSD support""" + try: + # Try to open PSD file - requires pillow-psd plugin + from psd_tools import PSDImage + psd = PSDImage.open(source_path) + pil_image = psd.composite() + + to_generate = (('SYNOPHOTO_THUMB_XL.jpg', 1280), + ('SYNOPHOTO_THUMB_B.jpg', 640), + ('SYNOPHOTO_THUMB_M.jpg', 320), + ('SYNOPHOTO_THUMB_PREVIEW.jpg', 160), + ('SYNOPHOTO_THUMB_S.jpg', 120)) + + for thumb in to_generate: + thumb_path = os.path.join(dest_dir, thumb[0]) + if os.path.exists(thumb_path): + continue + + pil_image.thumbnail((thumb[1], thumb[1]), Image.LANCZOS) + # Convert to RGB if needed for JPEG + if pil_image.mode == 'RGBA': + pil_image = pil_image.convert('RGB') + pil_image.save(thumb_path, 'JPEG') + + except ImportError: + print("Warning: psd-tools not installed. Install with: pip install psd-tools") + except Exception as e: + print(f"Error processing PSD {source_path}: {e}") + + +def extract_blend_thumbnail(blend_path): + """Extracts the largest PNG thumbnail from all PREV blocks in a .blend file.""" + import struct, re + with open(blend_path, 'rb') as f: + data = f.read() + + if not data.startswith(b'BLENDER'): + return None + pointer_size = 8 if data[7:8] == b'-' else 4 + endian = '<' if data[8:9] == b'v' else '>' + offset = 12 + candidates = [] + while offset + 16 < len(data): + code = data[offset:offset+4] + size = struct.unpack(endian + 'I', data[offset+4:offset+8])[0] + block_data_start = offset + 16 + pointer_size + block_data_end = block_data_start + size + if code == b'PREV': + block = data[block_data_start:block_data_end] + # Find all PNGs in this block + for m in re.finditer(b'\x89PNG\r\n\x1a\n', block): + start = m.start() + end = block.find(b'IEND', start) + if end != -1: + end += 8 + png_data = block[start:end] + try: + img = Image.open(io.BytesIO(png_data)) + candidates.append((img.size[0] * img.size[1], img)) + except Exception: + continue + offset = block_data_end + if not candidates: + return None + return max(candidates, key=lambda x: x[0])[1] + + +def create_blend_thumbnails(source_path, dest_dir): + """Extract embedded Blender thumbnail and generate Synology thumbnails.""" + img = extract_blend_thumbnail(source_path) + if img is None: + print(f"No embedded thumbnail in {source_path}") + return to_generate = (('SYNOPHOTO_THUMB_XL.jpg', 1280), ('SYNOPHOTO_THUMB_B.jpg', 640), @@ -106,11 +236,39 @@ def create_thumbnails(source_path, dest_dir): ('SYNOPHOTO_THUMB_S.jpg', 120)) for thumb in to_generate: - if os.path.exists(os.path.join(dest_dir, thumb[0])): + thumb_path = os.path.join(dest_dir, thumb[0]) + if os.path.exists(thumb_path): continue + im_copy = img.copy() + im_copy.thumbnail((thumb[1], thumb[1]), Image.LANCZOS) + if im_copy.mode == 'RGBA': + im_copy = im_copy.convert('RGB') + im_copy.save(thumb_path, 'JPEG', quality=85) - im.thumbnail((thumb[1], thumb[1]), Image.ANTIALIAS) - im.save(os.path.join(dest_dir, thumb[0])) + +def create_thumbnails(source_path, dest_dir): + """Original image thumbnail creation""" + try: + im = Image.open(source_path) + + to_generate = (('SYNOPHOTO_THUMB_XL.jpg', 1280), + ('SYNOPHOTO_THUMB_B.jpg', 640), + ('SYNOPHOTO_THUMB_M.jpg', 320), + ('SYNOPHOTO_THUMB_PREVIEW.jpg', 160), + ('SYNOPHOTO_THUMB_S.jpg', 120)) + + for thumb in to_generate: + thumb_path = os.path.join(dest_dir, thumb[0]) + if os.path.exists(thumb_path): + continue + + # Create a copy for thumbnailing + im_copy = im.copy() + im_copy.thumbnail((thumb[1], thumb[1]), Image.LANCZOS) + im_copy.save(thumb_path) + + except Exception as e: + print(f"Error processing image {source_path}: {e}") if __name__ == "__main__": diff --git a/run_thumbgen_prompt.bat b/run_thumbgen_prompt.bat new file mode 100644 index 0000000..d39c211 --- /dev/null +++ b/run_thumbgen_prompt.bat @@ -0,0 +1,8 @@ +@echo off +set /p NASDIR=Enter the full path to your NAS directory: +if "%NASDIR%"=="" ( + echo No directory provided. Exiting. + exit /b +) +python psthumbgen.py --directory "%NASDIR%" +pause \ No newline at end of file