Compare commits

...

3 Commits

Author SHA1 Message Date
Raincloud d2239a670c handle nested dirs 2026-03-22 13:35:25 -06:00
Raincloud 53430f768f color coded cli output! 2026-03-22 12:58:19 -06:00
Raincloud a8205447e3 refactor error handling and guidance 2026-03-22 12:51:08 -06:00
6 changed files with 4579 additions and 164 deletions
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -46,13 +46,13 @@
"last_updated": "2026-03-22T18:11:22Z"
},
"88555ca4-9c88-4bcf-825f-dd19b19b7145": {
"user_message_count": 35,
"agent_message_count": 341,
"user_message_count": 50,
"agent_message_count": 592,
"start_timestamp": "2026-03-21T13:01:32-06:00",
"end_timestamp": "2026-03-21T13:01:32-06:00",
"markdown_size_bytes": 310211,
"markdown_size_bytes": 461149,
"provider": "cursoride",
"last_updated": "2026-03-22T18:11:22Z"
"last_updated": "2026-03-22T19:25:16Z"
},
"8a226504-a2e0-4b10-b083-a609486034c7": {
"user_message_count": 9,
@@ -70,7 +70,7 @@
"end_timestamp": "2025-06-25T22:09:21-06:00",
"markdown_size_bytes": 38319,
"provider": "cursoride",
"last_updated": "2026-03-22T18:11:10Z"
"last_updated": "2026-03-22T18:13:12Z"
},
"b90a47f3-3c59-4ca3-a099-690412afaab1": {
"user_message_count": 7,
+90 -54
View File
@@ -1,31 +1,44 @@
@echo off
setlocal
setlocal EnableExtensions
REM ANSI colors (needs Windows 10+ console VT or Windows Terminal)
for /f %%a in ('powershell -nop -c "[char]0x1b"') do set "ESC=%%a"
set "RST=%ESC%[0m"
set "BLD=%ESC%[1m"
set "DIM=%ESC%[2m"
set "RED=%ESC%[91m"
set "GRN=%ESC%[92m"
set "YLW=%ESC%[93m"
set "BLU=%ESC%[94m"
set "MAG=%ESC%[95m"
set "CYN=%ESC%[96m"
set "GRY=%ESC%[90m"
set "WHT=%ESC%[97m"
REM Always use 'nathan' as NAS username
set NASUSER=nathan
REM Menu for operation selection
echo.
echo Select operation:
echo 1. Generate thumbnails only
echo 2. SSH cleanup only
echo 3. Both (generate thumbnails + SSH cleanup)
echo 4. Fix OneDrive thumbnail override (NEW!)
echo %BLD%%YLW%Select operation:%RST%
echo %CYN%1.%RST% Generate thumbnails only
echo %CYN%2.%RST% SSH cleanup only
echo %CYN%3.%RST% Both (generate thumbnails + SSH cleanup)
echo %CYN%4.%RST% Fix OneDrive thumbnail override %DIM%(NEW^)%RST%
echo.
set /p CHOICE=Enter your choice (1-4):
set /p CHOICE=%CYN%Enter your choice ^(1-4^): %RST%
if "%CHOICE%"=="1" goto THUMBS_ONLY
if "%CHOICE%"=="2" goto SSH_ONLY
if "%CHOICE%"=="3" goto BOTH
if "%CHOICE%"=="4" goto FIX_ONEDRIVE
echo Invalid choice. Exiting.
echo %RED%Invalid choice. Exiting.%RST%
exit /b
:THUMBS_ONLY
REM Prompt for Windows path
set /p WINPATH=Enter the full Windows path to your NAS directory (e.g., R:\YouTube\Streams\MixerTwitch):
set /p WINPATH=%CYN%Enter the full Windows path to your NAS directory ^(e.g., R:\YouTube\Streams\MixerTwitch^): %RST%
if "%WINPATH%"=="" (
echo No path provided. Exiting.
echo %RED%No path provided. Exiting.%RST%
exit /b
)
@@ -34,9 +47,9 @@ set "RELPATH=%WINPATH:~3%"
set "RELPATH=%RELPATH:\=/%"
set "NASPATH=/volume1/Hydra/%RELPATH%"
echo Uploading extract_eadir_to_tmp.sh to NAS and seeding...
echo %GRN%Uploading extract_eadir_to_tmp.sh to NAS and seeding...%RST%
if not exist "%~dp0extract_eadir_to_tmp.sh" (
echo ERROR: extract_eadir_to_tmp.sh not found next to this batch file.
echo %RED%ERROR: extract_eadir_to_tmp.sh not found next to this batch file.%RST%
goto END
)
ssh Hydra "cat > ~/extract_eadir_to_tmp.sh" < "%~dp0extract_eadir_to_tmp.sh"
@@ -48,9 +61,9 @@ goto END
:SSH_ONLY
REM Prompt for Windows path
set /p WINPATH=Enter the full Windows path to your NAS directory (e.g., R:\YouTube\Streams\MixerTwitch):
set /p WINPATH=%CYN%Enter the full Windows path to your NAS directory ^(e.g., R:\YouTube\Streams\MixerTwitch^): %RST%
if "%WINPATH%"=="" (
echo No path provided. Exiting.
echo %RED%No path provided. Exiting.%RST%
exit /b
)
@@ -59,27 +72,33 @@ set "RELPATH=%WINPATH:~3%"
set "RELPATH=%RELPATH:\=/%"
set "NASPATH=/volume1/Hydra/%RELPATH%"
echo DEBUG: WINPATH = %WINPATH%
echo DEBUG: RELPATH = %RELPATH%
echo DEBUG: NASPATH = %NASPATH%
echo %GRY%DEBUG: WINPATH = %WINPATH%%RST%
echo %GRY%DEBUG: RELPATH = %RELPATH%%RST%
echo %GRY%DEBUG: NASPATH = %NASPATH%%RST%
echo.
call :WIN_REMOVE_LOCAL_EADIR
REM Ensure nascleanup.sh exists on NAS (upload + fix line endings), then run it
echo Uploading nascleanup.sh to NAS and running cleanup...
echo %GRN%Uploading nascleanup.sh to NAS and running cleanup...%RST%
if not exist "%~dp0nascleanup.sh" (
echo ERROR: nascleanup.sh not found next to this batch file.
echo %RED%ERROR: nascleanup.sh not found next to this batch file.%RST%
goto END
)
ssh Hydra "cat > ~/nascleanup.sh" < "%~dp0nascleanup.sh"
ssh Hydra "bash -lc 'if command -v dos2unix >/dev/null 2>&1; then dos2unix -f ~/nascleanup.sh; else tr -d \"\r\" < ~/nascleanup.sh > ~/nascleanup.sh.tmp && mv ~/nascleanup.sh.tmp ~/nascleanup.sh; fi; chmod +x ~/nascleanup.sh; ~/nascleanup.sh \"%NASPATH%\"'"
call :WIN_REMOVE_LOCAL_EADIR_TMP
if errorlevel 1 (
echo %RED%ERROR: nascleanup failed on NAS - check promote ^(eaDir_tmp -^> @eaDir^) messages above.%RST%
echo %YLW%Keeping eaDir_tmp on the share so you can retry without re-running FFmpeg.%RST%
call :PRINT_HYDRA_CLEAR_EADIR
) else (
call :WIN_REMOVE_LOCAL_EADIR_TMP
)
goto END
:BOTH
REM Prompt for Windows path
set /p WINPATH=Enter the full Windows path to your NAS directory (e.g., R:\YouTube\Streams\MixerTwitch):
set /p WINPATH=%CYN%Enter the full Windows path to your NAS directory ^(e.g., R:\YouTube\Streams\MixerTwitch^): %RST%
if "%WINPATH%"=="" (
echo No path provided. Exiting.
echo %RED%No path provided. Exiting.%RST%
exit /b
)
@@ -88,15 +107,15 @@ set "RELPATH=%WINPATH:~3%"
set "RELPATH=%RELPATH:\=/%"
set "NASPATH=/volume1/Hydra/%RELPATH%"
echo DEBUG: WINPATH = %WINPATH%
echo DEBUG: RELPATH = %RELPATH%
echo DEBUG: NASPATH = %NASPATH%
echo %GRY%DEBUG: WINPATH = %WINPATH%%RST%
echo %GRY%DEBUG: RELPATH = %RELPATH%%RST%
echo %GRY%DEBUG: NASPATH = %NASPATH%%RST%
echo.
REM Pre-seed eaDir_tmp from @eaDir on NAS before generation
echo Uploading extract_eadir_to_tmp.sh to NAS and seeding...
echo %GRN%Uploading extract_eadir_to_tmp.sh to NAS and seeding...%RST%
if not exist "%~dp0extract_eadir_to_tmp.sh" (
echo ERROR: extract_eadir_to_tmp.sh not found next to this batch file.
echo %RED%ERROR: extract_eadir_to_tmp.sh not found next to this batch file.%RST%
goto END
)
ssh Hydra "cat > ~/extract_eadir_to_tmp.sh" < "%~dp0extract_eadir_to_tmp.sh"
@@ -110,41 +129,47 @@ call :WIN_REMOVE_LOCAL_EADIR
REM SSH cleanup commands (run separately)
REM Ensure nascleanup.sh exists on NAS (upload + fix line endings), then run it
echo Uploading nascleanup.sh to NAS and running cleanup...
echo %GRN%Uploading nascleanup.sh to NAS and running cleanup...%RST%
if not exist "%~dp0nascleanup.sh" (
echo ERROR: nascleanup.sh not found next to this batch file.
echo %RED%ERROR: nascleanup.sh not found next to this batch file.%RST%
goto END
)
ssh Hydra "cat > ~/nascleanup.sh" < "%~dp0nascleanup.sh"
ssh Hydra "bash -lc 'if command -v dos2unix >/dev/null 2>&1; then dos2unix -f ~/nascleanup.sh; else tr -d \"\r\" < ~/nascleanup.sh > ~/nascleanup.sh.tmp && mv ~/nascleanup.sh.tmp ~/nascleanup.sh; fi; chmod +x ~/nascleanup.sh; ~/nascleanup.sh \"%NASPATH%\"'"
call :WIN_REMOVE_LOCAL_EADIR_TMP
if errorlevel 1 (
echo %RED%ERROR: nascleanup failed on NAS - check promote ^(eaDir_tmp -^> @eaDir^) messages above.%RST%
echo %YLW%Keeping eaDir_tmp on the share so you can retry without re-running FFmpeg.%RST%
call :PRINT_HYDRA_CLEAR_EADIR
) else (
call :WIN_REMOVE_LOCAL_EADIR_TMP
)
goto END
:FIX_ONEDRIVE
echo.
echo =================================================================
echo FIX ONEDRIVE THUMBNAIL OVERRIDE
echo =================================================================
echo %BLD%%MAG%=================================================================%RST%
echo %BLD%%MAG% FIX ONEDRIVE THUMBNAIL OVERRIDE %RST%
echo %BLD%%MAG%=================================================================%RST%
echo.
echo This will fix OneDrive overriding your custom thumbnails with
echo generic file icons for unsupported formats like .blend files.
echo %WHT%This will fix OneDrive overriding your custom thumbnails with%RST%
echo %WHT%generic file icons for unsupported formats like .blend files.%RST%
echo.
echo What this does:
echo - Disables OneDrive cloud thumbnail providers
echo - Restores local thumbnail priority
echo - Prevents future thumbnail cache deletion
echo %CYN%What this does:%RST%
echo %GRY% - Disables OneDrive cloud thumbnail providers%RST%
echo %GRY% - Restores local thumbnail priority%RST%
echo %GRY% - Prevents future thumbnail cache deletion%RST%
echo.
set /p CONFIRM=Continue? (y/n):
set /p CONFIRM=%YLW%Continue? ^(y/n^): %RST%
if /i not "%CONFIRM%"=="y" goto END
echo.
echo Running PowerShell script to fix OneDrive thumbnail override...
echo %GRN%Running PowerShell script to fix OneDrive thumbnail override...%RST%
echo.
REM Check if PowerShell script exists
if not exist "fix_thumbnail_override.ps1" (
echo ERROR: fix_thumbnail_override.ps1 not found in current directory.
echo Please ensure the PowerShell script is in the same folder as this batch file.
echo %RED%ERROR: fix_thumbnail_override.ps1 not found in current directory.%RST%
echo %YLW%Please ensure the PowerShell script is in the same folder as this batch file.%RST%
pause
goto END
)
@@ -153,24 +178,34 @@ REM Run PowerShell script as administrator
powershell -ExecutionPolicy Bypass -Command "Start-Process PowerShell -ArgumentList '-ExecutionPolicy Bypass -File \"%~dp0fix_thumbnail_override.ps1\"' -Verb RunAs"
echo.
echo OneDrive thumbnail override fix initiated.
echo Please check the PowerShell window for progress and results.
echo %GRN%OneDrive thumbnail override fix initiated.%RST%
echo %GRY%Please check the PowerShell window for progress and results.%RST%
echo.
echo After the fix completes:
echo 1. Restart your computer
echo 2. Re-run your thumbnail generation process
echo 3. Your custom thumbnails should now have priority!
echo %CYN%After the fix completes:%RST%
echo %GRY% 1. Restart your computer%RST%
echo %GRY% 2. Re-run your thumbnail generation process%RST%
echo %GRY% 3. Your custom thumbnails should now have priority!%RST%
echo.
goto END
REM Print ssh Hydra command to wipe @eaDir when NAS cleanup failed (matches NASPATH from pasted R:\ path).
:PRINT_HYDRA_CLEAR_EADIR
if "%NASPATH%"=="" goto :eof
echo %MAG%To clear locked @eaDir on Hydra: ssh in first.%RST%
echo %GRY%Permission denied lines above name nested paths ^(e.g. editDesc, gameplay^).%RST%
echo %GRY%Clear every @eaDir under the SAME folder you passed to nascleanup ^(NASPATH below^),%RST%
echo %GRY%not only the subpath from the first error line.%RST%
echo %BLD%%YLW% sudo find "%NASPATH%" -depth -type d -name '@eaDir' -exec sudo rm -rf {} +%RST%
goto :eof
REM Remove folder-level @eaDir on the pasted Windows path (same files as NAS over SMB).
:WIN_REMOVE_LOCAL_EADIR
if "%WINPATH%"=="" goto :eof
if exist "%WINPATH%\@eaDir" (
echo Removing "%WINPATH%\@eaDir" via Windows ^(SMB share^)...
echo %CYN%Removing "%WINPATH%\@eaDir" via Windows ^(SMB share^)...%RST%
rd /s /q "%WINPATH%\@eaDir" 2>nul
if exist "%WINPATH%\@eaDir" (
echo WARNING: Could not remove @eaDir; try PowerShell as admin or delete in Explorer.
echo %YLW%WARNING: Could not remove @eaDir; try PowerShell as admin or delete in Explorer.%RST%
)
)
goto :eof
@@ -178,13 +213,14 @@ goto :eof
REM Remove every eaDir_tmp under pasted path (including subfolders e.g. gameplay\eaDir_tmp).
:WIN_REMOVE_LOCAL_EADIR_TMP
if "%WINPATH%"=="" goto :eof
echo Removing all eaDir_tmp folders under "%WINPATH%" ^(recursive, SMB^)...
echo %CYN%Removing all eaDir_tmp folders under "%WINPATH%" ^(recursive, SMB^)...%RST%
if not exist "%~dp0remove_eadir_tmp_recursive.ps1" (
echo ERROR: remove_eadir_tmp_recursive.ps1 not found next to this batch file.
echo %RED%ERROR: remove_eadir_tmp_recursive.ps1 not found next to this batch file.%RST%
goto :eof
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0remove_eadir_tmp_recursive.ps1" "%WINPATH%"
goto :eof
:END
pause
echo %DIM%Press any key to continue . . .%RST%
pause >nul
+2 -2
View File
@@ -20,8 +20,8 @@ fi
echo "=== Extracting @eaDir thumbnails to eaDir_tmp ==="
echo "Target: $TARGET_DIR"
# Only TARGET_DIR/@eaDir (not nested .../@eaDir/.../@eaDir from bad prior runs; those hit chmod 555 parents)
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 -type d -name '@eaDir' -print0 | while IFS= read -r -d '' EADIR; do
# Every .../@eaDir under TARGET (e.g. TARGET/@eaDir and TARGET/gameplay/@eaDir). Skip paths inside eaDir_tmp.
find "$TARGET_DIR" -type d -name '@eaDir' ! -path '*/eaDir_tmp/*' -print0 | while IFS= read -r -d '' EADIR; do
PARENT_DIR="$(dirname "$EADIR")"
# Each immediate subdir under @eaDir corresponds to a source filename
+43 -27
View File
@@ -28,6 +28,38 @@ flatten_indexer_layout() {
done
}
# Copy one thumbgen tree: PARENT/eaDir_tmp -> PARENT/@eaDir (psthumbgen uses per-folder eaDir_tmp).
promote_one_eadir_tmp() {
local EADIR_TMP="$1"
local PARENT TARGET_AT SRC_JPG DST_JPG
PARENT="$(dirname "$EADIR_TMP")"
TARGET_AT="$PARENT/@eaDir"
[ -d "$EADIR_TMP" ] || return 0
chmod -R 755 "$EADIR_TMP" 2>/dev/null || true
find "$EADIR_TMP" -type f -exec chmod 644 '{}' \; 2>/dev/null || true
echo " -> Flattening eaDir_tmp (indexer may have added .../<file>/@eaDir/) before copy: $EADIR_TMP"
flatten_indexer_layout "$EADIR_TMP"
echo " -> Replacing $EADIR_TMP -> $TARGET_AT"
SRC_JPG=$(find "$EADIR_TMP" -type f -name 'SYNO*.jpg' 2>/dev/null | wc -l | tr -d '[:space:]')
rm -rf "$TARGET_AT"
mkdir -p "$TARGET_AT"
if ! cp -R "$EADIR_TMP/." "$TARGET_AT/"; then
echo "ERROR: eaDir_tmp -> @eaDir copy failed (see cp messages above)."
exit 1
fi
echo " -> Flattening @eaDir after copy (same layout fix)"
flatten_indexer_layout "$TARGET_AT"
DST_JPG=$(find "$TARGET_AT" -type f -name 'SYNO*.jpg' 2>/dev/null | wc -l | tr -d '[:space:]')
if [ "$SRC_JPG" -gt 0 ] && [ "$DST_JPG" -ne "$SRC_JPG" ]; then
echo "ERROR: Promote incomplete: $DST_JPG SYNO*.jpg in @eaDir vs $SRC_JPG in eaDir_tmp (expected equal)."
exit 1
fi
chmod -R 755 "$EADIR_TMP" 2>/dev/null || true
find "$EADIR_TMP" -type f -exec chmod u+w '{}' \; 2>/dev/null || true
find "$EADIR_TMP" -type d -exec chmod u+w '{}' \; 2>/dev/null || true
rm -rf "$EADIR_TMP" 2>/dev/null || sudo -n rm -rf "$EADIR_TMP" 2>/dev/null || true
}
echo "=== Enhanced Synology Thumbnail Cleanup ==="
echo "Target directory: $TARGET_DIR"
@@ -77,33 +109,18 @@ find "$TARGET_DIR" -path '*/@eaDir/*' -type f -exec chmod 644 '{}' \; 2>/dev/nul
# Now remove them (2>/dev/null: races when parents disappear mid-find)
find "$TARGET_DIR" -type d -name '@eaDir' -exec rm -rf '{}' \; 2>/dev/null || true
# 5. Promote only TARGET_DIR/eaDir_tmp -> TARGET_DIR/@eaDir (thumbgen layout; not nested */eaDir_tmp)
# 5. Promote every .../eaDir_tmp -> .../@eaDir (thumbgen writes eaDir_tmp next to each video folder, not only TARGET_DIR/eaDir_tmp)
echo "=== Installing custom thumbnails ==="
EADIR_TMP="$TARGET_DIR/eaDir_tmp"
if [ -d "$EADIR_TMP" ]; then
chmod -R 755 "$EADIR_TMP" 2>/dev/null || true
find "$EADIR_TMP" -type f -exec chmod 644 '{}' \; 2>/dev/null || true
echo " -> Flattening eaDir_tmp (indexer may have added .../<file>/@eaDir/) before copy"
flatten_indexer_layout "$EADIR_TMP"
TARGET_AT="$TARGET_DIR/@eaDir"
echo " -> Replacing $EADIR_TMP -> $TARGET_AT"
rm -rf "$TARGET_AT"
mkdir -p "$TARGET_AT"
cp -R "$EADIR_TMP/." "$TARGET_AT/"
echo " -> Flattening @eaDir after copy (same layout fix)"
flatten_indexer_layout "$TARGET_AT"
chmod -R 755 "$EADIR_TMP" 2>/dev/null || true
find "$EADIR_TMP" -type f -exec chmod u+w '{}' \; 2>/dev/null || true
find "$EADIR_TMP" -type d -exec chmod u+w '{}' \; 2>/dev/null || true
if ! rm -rf "$EADIR_TMP" 2>/dev/null; then
echo " -> rm eaDir_tmp failed; retrying with sudo -n"
sudo -n rm -rf "$EADIR_TMP" 2>/dev/null || echo " -> WARNING: remove stale eaDir_tmp manually: sudo rm -rf \"$EADIR_TMP\""
fi
else
echo " -> No $EADIR_TMP; skipping install step"
INSTALL_ANY=0
while IFS= read -r -d '' EADIR_TMP; do
INSTALL_ANY=1
promote_one_eadir_tmp "$EADIR_TMP"
done < <(find "$TARGET_DIR" -type d -name 'eaDir_tmp' -print0)
if [ "$INSTALL_ANY" -eq 0 ]; then
echo " -> No eaDir_tmp under $TARGET_DIR; skipping install step"
fi
echo "=== Removing stray nested eaDir_tmp directories ==="
find "$TARGET_DIR" -mindepth 2 -type d -name 'eaDir_tmp' -exec rm -rf '{}' \; 2>/dev/null || true
echo "=== Removing any leftover eaDir_tmp directories ==="
find "$TARGET_DIR" -type d -name 'eaDir_tmp' -exec rm -rf '{}' \; 2>/dev/null || true
# 6. Protect custom thumbnails with read-only permissions
echo "=== Adding indexing exclusion hints ==="
@@ -124,5 +141,4 @@ elif command -v systemctl >/dev/null 2>&1; then
sudo -n systemctl start "$u" 2>/dev/null || systemctl start "$u" 2>/dev/null || true
done
fi
echo "Note: If indexing was stopped and needs restarting, use DSM UI or: sudo systemctl restart synoindexd"
echo "If rm failed with Permission denied: delete @eaDir and eaDir_tmp on your pasted Windows path (SMB), or: sudo rm -rf ${TARGET_DIR}/eaDir_tmp ${TARGET_DIR}/@eaDir — then re-run option 3."
echo "Note: If indexing was stopped and needs restarting, use DSM UI or: sudo systemctl restart synoindexd"
+61 -34
View File
@@ -72,6 +72,29 @@ def _ffmpeg_enabled():
return False
_ANSI_BLUE = "\033[94m"
_ANSI_GREEN = "\033[92m"
_ANSI_RST = "\033[0m"
def _print_icaros(msg):
print(f"{_ANSI_BLUE}{msg}{_ANSI_RST}")
def _print_ffmpeg(msg):
print(f"{_ANSI_GREEN}{msg}{_ANSI_RST}")
def _ffmpeg_first_for_gameplay_path(path):
"""Under ...\\gameplay\\, Shell GetThumbnail often fails and Icaros cache misses; go straight to FFmpeg."""
if not _ffmpeg_enabled():
return False
try:
p = os.path.normpath(path).replace("/", "\\").lower()
except Exception:
return False
return "\\gameplay\\" in p
def find_files(dir):
# Only process formats that Synology doesn't handle well
@@ -133,16 +156,20 @@ def ensure_directory_exists(path):
def create_video_thumbnails(source_path, dest_dir):
"""Generate video thumbnails: Windows Shell (Icaros-backed) first, FFmpeg fallback."""
if _ffmpeg_first_for_gameplay_path(source_path):
_print_ffmpeg(f"Using FFmpeg directly (gameplay subtree): {source_path}")
create_video_thumbnails_ffmpeg(source_path, dest_dir)
return
# Try Windows thumbnail extraction first (leverages Icaros provider when present)
windows_thumb = extract_windows_thumbnail(source_path)
if windows_thumb:
print(f" -> SUCCESS: Using Windows/Icaros provider")
_print_icaros(f" -> SUCCESS: Using Windows/Icaros provider")
generate_synology_thumbnails(windows_thumb, dest_dir, include_video_screenshot=True)
return
# Optionally fall back to FFmpeg (disabled by default)
if _ffmpeg_enabled():
print(f"Windows/Icaros extraction failed for {source_path}, using FFmpeg...")
_print_ffmpeg(f"Windows/Icaros extraction failed for {source_path}, using FFmpeg...")
create_video_thumbnails_ffmpeg(source_path, dest_dir)
else:
print(f"Windows/Icaros extraction failed for {source_path}, FFmpeg disabled (THUMBGEN_ENABLE_FFMPEG=0). Skipping.")
@@ -175,15 +202,15 @@ def create_video_thumbnails_ffmpeg(source_path, dest_dir):
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Warning: FFmpeg failed for {source_path}: {result.stderr}")
_print_ffmpeg(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")
_print_ffmpeg("Warning: FFmpeg not found. Video thumbnails will be skipped.")
_print_ffmpeg("Install FFmpeg: https://ffmpeg.org/download.html")
break
except Exception as e:
print(f"Error processing video {source_path}: {e}")
_print_ffmpeg(f"Error processing video {source_path}: {e}")
def create_psd_thumbnails(source_path, dest_dir):
@@ -315,15 +342,15 @@ def extract_windows_thumbnail(file_path):
# Tier 3: Try Icaros cache extraction
try:
print(f" -> Trying Icaros cache extraction...")
_print_icaros(f" -> Trying Icaros cache extraction...")
icaros_thumb = extract_icaros_thumbnail(file_path)
if icaros_thumb:
print(f" -> Found thumbnail in Icaros cache for {filename}")
_print_icaros(f" -> Found thumbnail in Icaros cache for {filename}")
return icaros_thumb
else:
print(f" -> No thumbnail in Icaros cache for {filename}")
_print_icaros(f" -> No thumbnail in Icaros cache for {filename}")
except Exception as e:
print(f" -> Icaros cache extraction failed: {e}")
_print_icaros(f" -> Icaros cache extraction failed: {e}")
print(f" -> Windows thumbnail extraction failed for {filename}")
return None
@@ -679,28 +706,28 @@ def extract_icaros_thumbnail(file_path):
# Locate Icaros cache directory
icaros_cache_dir = _get_icaros_cache_dir()
if not icaros_cache_dir:
print(f" -> No Icaros cache directory found")
_print_icaros(f" -> No Icaros cache directory found")
return None
# Debug: Show cache directory contents (all files)
try:
cache_files = os.listdir(icaros_cache_dir)
print(f" -> Icaros cache has {len(cache_files)} files")
_print_icaros(f" -> Icaros cache has {len(cache_files)} files")
if len(cache_files) > 0:
print(f" -> All cache files: {cache_files}")
_print_icaros(f" -> All cache files: {cache_files}")
except Exception:
pass
# Discover .icdb databases by size preference
icdb_paths = _list_icdb_databases(icaros_cache_dir)
if not icdb_paths:
print(f" -> No .icdb database files found")
_print_icaros(f" -> No .icdb database files found")
return None
# Lookup a precise index for this file from Icaros_idx.icdb (SQLite or binary)
mapped_index, preferred_db = _lookup_icaros_index(icaros_cache_dir, file_path)
if mapped_index is None:
print(f" -> No exact Icaros index entry for this file; skipping Icaros")
_print_icaros(f" -> No exact Icaros index entry for this file; skipping Icaros")
return None
# Try preferred DB first if provided
@@ -711,16 +738,16 @@ def extract_icaros_thumbnail(file_path):
for icdb_path in ordered_dbs:
img = extract_from_icdb_database(icdb_path, file_path, forced_index=mapped_index)
if img:
print(f" -> Found thumbnail in {os.path.basename(icdb_path)} via mapped index")
_print_icaros(f" -> Found thumbnail in {os.path.basename(icdb_path)} via mapped index")
return img
print(f" -> Mapped index did not resolve a thumbnail in any DB")
_print_icaros(f" -> Mapped index did not resolve a thumbnail in any DB")
return None
except ImportError:
print(f" -> Required modules not available for .icdb extraction")
_print_icaros(f" -> Required modules not available for .icdb extraction")
return None
except Exception as e:
print(f" -> Icaros cache extraction error: {e}")
_print_icaros(f" -> Icaros cache extraction error: {e}")
return None
@@ -814,12 +841,12 @@ def _build_icaros_index_map(cache_dir):
conn.close()
_ICAROS_INDEX_CACHE = mapping
if mapping:
print(f" -> Loaded Icaros index map for {len(mapping)} files")
_print_icaros(f" -> Loaded Icaros index map for {len(mapping)} files")
else:
print(f" -> Icaros index database present but no usable mapping found")
_print_icaros(f" -> Icaros index database present but no usable mapping found")
return _ICAROS_INDEX_CACHE
except Exception as e:
print(f" -> Failed to read Icaros index: {e}")
_print_icaros(f" -> Failed to read Icaros index: {e}")
_ICAROS_INDEX_CACHE = {}
return _ICAROS_INDEX_CACHE
@@ -988,7 +1015,7 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
# Verify ICDB signature
if not data.startswith(b'ICDB'):
print(f" -> Invalid ICDB signature in {icdb_path}")
_print_icaros(f" -> Invalid ICDB signature in {icdb_path}")
return None
# Search for JPEG images in the file
@@ -1002,10 +1029,10 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
pos += 1
if not jpeg_positions:
print(f" -> No JPEG images found in {icdb_path}")
_print_icaros(f" -> No JPEG images found in {icdb_path}")
return None
print(f" -> Found {len(jpeg_positions)} JPEG images in {icdb_path}")
_print_icaros(f" -> Found {len(jpeg_positions)} JPEG images in {icdb_path}")
# If we have a mapped index, try it directly first
if isinstance(forced_index, int) and len(jpeg_positions) > 0:
@@ -1021,11 +1048,11 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
jpeg_data = data[jpeg_start:jpeg_end]
try:
img = Image.open(io.BytesIO(jpeg_data))
print(f" -> Used mapped index {pos_index} from {os.path.basename(icdb_path)}")
_print_icaros(f" -> Used mapped index {pos_index} from {os.path.basename(icdb_path)}")
return img.copy()
except Exception:
# If mapped position invalid, continue to heuristic
print(f" -> Mapped index {pos_index} invalid; falling back to heuristic")
_print_icaros(f" -> Mapped index {pos_index} invalid; falling back to heuristic")
# Heuristic alphabetical mapping (fallback)
def get_alphabetical_position(target_file_path):
@@ -1079,10 +1106,10 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
try:
position = all_files.index(target_rel_path)
print(f" -> Alphabetical position estimate: {position} for {target_filename}")
_print_icaros(f" -> Alphabetical position estimate: {position} for {target_filename}")
return position
except ValueError:
print(f" -> File {target_filename} not found in current monitored directories")
_print_icaros(f" -> File {target_filename} not found in current monitored directories")
# Return a reasonable fallback position
return len(all_files) // 2 # Middle position as fallback
@@ -1093,16 +1120,16 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
if cache_position >= len(jpeg_positions):
# Use modulo to wrap around (cache might be from larger file set)
cache_position = cache_position % len(jpeg_positions)
print(f" -> Wrapped position to {cache_position} (cache has {len(jpeg_positions)} entries)")
_print_icaros(f" -> Wrapped position to {cache_position} (cache has {len(jpeg_positions)} entries)")
jpeg_start = jpeg_positions[cache_position]
print(f" -> Alphabetical mapping -> JPEG {cache_position} for {os.path.basename(file_path)}")
_print_icaros(f" -> Alphabetical mapping -> JPEG {cache_position} for {os.path.basename(file_path)}")
# Find the end of the JPEG (FF D9 marker)
jpeg_end = data.find(b'\xff\xd9', jpeg_start)
if jpeg_end == -1:
print(f" -> Could not find JPEG end marker")
_print_icaros(f" -> Could not find JPEG end marker")
return None
jpeg_end += 2 # Include the end marker
@@ -1110,11 +1137,11 @@ def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
# Try to create PIL Image from the JPEG data
img = Image.open(io.BytesIO(jpeg_data))
print(f" -> Successfully extracted {img.size[0]}x{img.size[1]} JPEG from {icdb_path}")
_print_icaros(f" -> Successfully extracted {img.size[0]}x{img.size[1]} JPEG from {icdb_path}")
return img.copy()
except Exception as e:
print(f" -> Error reading ICDB file {icdb_path}: {e}")
_print_icaros(f" -> Error reading ICDB file {icdb_path}: {e}")
return None