rewrite icaros cache handling
This commit is contained in:
File diff suppressed because it is too large
Load Diff
449
psthumbgen.py
449
psthumbgen.py
@@ -64,6 +64,13 @@ def parse_args():
|
|||||||
"Subdirectories will always be processed.")
|
"Subdirectories will always be processed.")
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
def _ffmpeg_enabled():
|
||||||
|
try:
|
||||||
|
val = os.environ.get('THUMBGEN_ENABLE_FFMPEG', '1').strip().lower()
|
||||||
|
return val in ('1', 'true', 'yes', 'on')
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def find_files(dir):
|
def find_files(dir):
|
||||||
@@ -125,38 +132,20 @@ def ensure_directory_exists(path):
|
|||||||
|
|
||||||
|
|
||||||
def create_video_thumbnails(source_path, dest_dir):
|
def create_video_thumbnails(source_path, dest_dir):
|
||||||
"""Generate video thumbnails using Icaros cache first, then Windows provider, then FFmpeg fallback"""
|
"""Generate video thumbnails: Windows Shell (Icaros-backed) first, FFmpeg fallback."""
|
||||||
file_ext = os.path.splitext(source_path)[1].lower()
|
# Try Windows thumbnail extraction first (leverages Icaros provider when present)
|
||||||
|
windows_thumb = extract_windows_thumbnail(source_path)
|
||||||
# Skip Icaros cache extraction - cache doesn't match current files
|
if windows_thumb:
|
||||||
# TODO: Re-enable after cache rebuild with current file set
|
print(f" -> SUCCESS: Using Windows/Icaros provider")
|
||||||
# print(f"Trying Icaros cache extraction for {os.path.basename(source_path)}...")
|
generate_synology_thumbnails(windows_thumb, dest_dir, include_video_screenshot=True)
|
||||||
# icaros_thumb = extract_icaros_thumbnail(source_path)
|
|
||||||
# if icaros_thumb:
|
|
||||||
# print(f" -> SUCCESS: Using Icaros cache thumbnail")
|
|
||||||
# generate_synology_thumbnails(icaros_thumb, dest_dir, include_video_screenshot=True)
|
|
||||||
# return
|
|
||||||
|
|
||||||
# Skip Windows extraction for formats that typically don't work
|
|
||||||
unsupported_formats = ['.m2ts', '.mts', '.flv', '.webm']
|
|
||||||
|
|
||||||
if file_ext not in unsupported_formats:
|
|
||||||
# Try Windows thumbnail extraction second (still faster than FFmpeg)
|
|
||||||
print(f"Icaros cache failed, trying Windows thumbnail extraction...")
|
|
||||||
windows_thumb = extract_windows_thumbnail(source_path)
|
|
||||||
if windows_thumb:
|
|
||||||
print(f" -> SUCCESS: Using Windows thumbnail provider")
|
|
||||||
generate_synology_thumbnails(windows_thumb, dest_dir, include_video_screenshot=True)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print(f"Skipping Windows thumbnail extraction for {file_ext} format, using FFmpeg...")
|
|
||||||
# Fallback to FFmpeg
|
|
||||||
create_video_thumbnails_ffmpeg(source_path, dest_dir)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only reach here if both Icaros and Windows extraction failed
|
# Optionally fall back to FFmpeg (disabled by default)
|
||||||
print(f"Both Icaros and Windows extraction failed for {source_path}, using FFmpeg...")
|
if _ffmpeg_enabled():
|
||||||
create_video_thumbnails_ffmpeg(source_path, dest_dir)
|
print(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.")
|
||||||
|
|
||||||
|
|
||||||
def create_video_thumbnails_ffmpeg(source_path, dest_dir):
|
def create_video_thumbnails_ffmpeg(source_path, dest_dir):
|
||||||
@@ -213,8 +202,8 @@ def create_psd_thumbnails(source_path, dest_dir):
|
|||||||
def create_psd_thumbnails_direct(source_path, dest_dir):
|
def create_psd_thumbnails_direct(source_path, dest_dir):
|
||||||
"""Generate PSD thumbnails using PIL with PSD support (fallback method)"""
|
"""Generate PSD thumbnails using PIL with PSD support (fallback method)"""
|
||||||
try:
|
try:
|
||||||
# Try to open PSD file - requires pillow-psd plugin
|
# Try to open PSD file - requires psd-tools
|
||||||
from psd_tools import PSDImage
|
from psd_tools import PSDImage # type: ignore[reportMissingImports]
|
||||||
psd = PSDImage.open(source_path)
|
psd = PSDImage.open(source_path)
|
||||||
pil_image = psd.composite()
|
pil_image = psd.composite()
|
||||||
|
|
||||||
@@ -306,29 +295,35 @@ def extract_windows_thumbnail(file_path):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" -> Windows thumbcache extraction failed: {e}")
|
print(f" -> Windows thumbcache extraction failed: {e}")
|
||||||
|
|
||||||
# TEMPORARILY DISABLED: Tier 2: Try Thumbs.db extraction
|
# Tier 2: Try Thumbs.db extraction
|
||||||
print(f" -> DISABLED: Skipping Thumbs.db extraction for debugging")
|
try:
|
||||||
# try:
|
directory = os.path.dirname(file_path)
|
||||||
# directory = os.path.dirname(file_path)
|
thumbs_db_path = os.path.join(directory, 'Thumbs.db')
|
||||||
# thumbs_db_path = os.path.join(directory, 'Thumbs.db')
|
|
||||||
#
|
|
||||||
# if os.path.exists(thumbs_db_path):
|
|
||||||
# print(f" -> Found Thumbs.db, checking for {filename}")
|
|
||||||
# thumb = extract_from_thumbs_db(thumbs_db_path, filename)
|
|
||||||
# if thumb:
|
|
||||||
# print(f" -> Found thumbnail in Thumbs.db for {filename}")
|
|
||||||
# return thumb
|
|
||||||
# else:
|
|
||||||
# print(f" -> No thumbnail in Thumbs.db for {filename}")
|
|
||||||
# else:
|
|
||||||
# print(f" -> No Thumbs.db found in directory")
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f" -> Thumbs.db extraction failed: {e}")
|
|
||||||
|
|
||||||
# TEMPORARILY DISABLED: Tier 3: Skip Icaros cache (algorithm produces random results)
|
if os.path.exists(thumbs_db_path):
|
||||||
print(f" -> DISABLED: Skipping Icaros cache for debugging")
|
print(f" -> Found Thumbs.db, checking for {filename}")
|
||||||
# TODO: Properly reverse engineer Icaros cache mapping
|
thumb = extract_from_thumbs_db(thumbs_db_path, filename)
|
||||||
# print(f" -> Skipping Icaros cache (mapping algorithm incomplete)")
|
if thumb:
|
||||||
|
print(f" -> Found thumbnail in Thumbs.db for {filename}")
|
||||||
|
return thumb
|
||||||
|
else:
|
||||||
|
print(f" -> No thumbnail in Thumbs.db for {filename}")
|
||||||
|
else:
|
||||||
|
print(f" -> No Thumbs.db found in directory")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" -> Thumbs.db extraction failed: {e}")
|
||||||
|
|
||||||
|
# Tier 3: Try Icaros cache extraction
|
||||||
|
try:
|
||||||
|
print(f" -> Trying Icaros cache extraction...")
|
||||||
|
icaros_thumb = extract_icaros_thumbnail(file_path)
|
||||||
|
if icaros_thumb:
|
||||||
|
print(f" -> Found thumbnail in Icaros cache for {filename}")
|
||||||
|
return icaros_thumb
|
||||||
|
else:
|
||||||
|
print(f" -> No thumbnail in Icaros cache for {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" -> Icaros cache extraction failed: {e}")
|
||||||
|
|
||||||
print(f" -> Windows thumbnail extraction failed for {filename}")
|
print(f" -> Windows thumbnail extraction failed for {filename}")
|
||||||
return None
|
return None
|
||||||
@@ -454,7 +449,7 @@ try {{
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
"@
|
"@ -ReferencedAssemblies System.Drawing
|
||||||
|
|
||||||
Write-Output "DEBUG: Type definitions loaded, calling GetThumbnail..."
|
Write-Output "DEBUG: Type definitions loaded, calling GetThumbnail..."
|
||||||
|
|
||||||
@@ -480,9 +475,10 @@ try {{
|
|||||||
|
|
||||||
print(f" -> Executing PowerShell script...")
|
print(f" -> Executing PowerShell script...")
|
||||||
# Execute PowerShell script
|
# Execute PowerShell script
|
||||||
result = subprocess.run([
|
# Force Windows PowerShell 5.1 (powershell.exe) instead of pwsh
|
||||||
"powershell", "-Command", powershell_script
|
ps_exe = "powershell.exe" if os.name == 'nt' else "powershell"
|
||||||
], capture_output=True, text=True, timeout=30)
|
result = subprocess.run([ps_exe, "-NoProfile", "-Command", powershell_script],
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
|
||||||
print(f" -> PowerShell return code: {result.returncode}")
|
print(f" -> PowerShell return code: {result.returncode}")
|
||||||
print(f" -> PowerShell stdout: {result.stdout}")
|
print(f" -> PowerShell stdout: {result.stdout}")
|
||||||
@@ -571,7 +567,7 @@ try {{
|
|||||||
def extract_from_thumbs_db(thumbs_db_path, target_filename):
|
def extract_from_thumbs_db(thumbs_db_path, target_filename):
|
||||||
"""Extract specific file thumbnail from Thumbs.db"""
|
"""Extract specific file thumbnail from Thumbs.db"""
|
||||||
try:
|
try:
|
||||||
import olefile
|
import olefile # type: ignore[reportMissingModuleSource]
|
||||||
|
|
||||||
if not olefile.isOleFile(thumbs_db_path):
|
if not olefile.isOleFile(thumbs_db_path):
|
||||||
return None
|
return None
|
||||||
@@ -680,20 +676,9 @@ def extract_icaros_thumbnail(file_path):
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# Icaros stores thumbnails in .icdb database files
|
# Locate Icaros cache directory
|
||||||
icaros_cache_dir = r"C:\Program Files\Icaros\IcarosCache"
|
icaros_cache_dir = _get_icaros_cache_dir()
|
||||||
if not os.path.exists(icaros_cache_dir):
|
if not icaros_cache_dir:
|
||||||
# Try alternative common locations
|
|
||||||
alt_locations = [
|
|
||||||
r"C:\Program Files (x86)\Icaros\IcarosCache",
|
|
||||||
os.path.expanduser(r"~\AppData\Local\Icaros\IcarosCache"),
|
|
||||||
os.path.expanduser(r"~\AppData\Roaming\Icaros\IcarosCache")
|
|
||||||
]
|
|
||||||
for alt_dir in alt_locations:
|
|
||||||
if os.path.exists(alt_dir):
|
|
||||||
icaros_cache_dir = alt_dir
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print(f" -> No Icaros cache directory found")
|
print(f" -> No Icaros cache directory found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -706,34 +691,29 @@ def extract_icaros_thumbnail(file_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Look for .icdb database files
|
# Discover .icdb databases by size preference
|
||||||
icdb_files = [f for f in os.listdir(icaros_cache_dir) if f.endswith('.icdb')]
|
icdb_paths = _list_icdb_databases(icaros_cache_dir)
|
||||||
if not icdb_files:
|
if not icdb_paths:
|
||||||
print(f" -> No .icdb database files found")
|
print(f" -> No .icdb database files found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Try to extract from the largest database first (likely has the best quality)
|
# Lookup a precise index for this file from Icaros_idx.icdb (SQLite or binary)
|
||||||
def get_size_from_filename(filename):
|
mapped_index, preferred_db = _lookup_icaros_index(icaros_cache_dir, file_path)
|
||||||
try:
|
if mapped_index is None:
|
||||||
if '_' in filename and filename.endswith('.icdb'):
|
print(f" -> No exact Icaros index entry for this file; skipping Icaros")
|
||||||
return int(filename.split('_')[1].split('.')[0])
|
return None
|
||||||
return 0
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
icdb_files.sort(key=get_size_from_filename, reverse=True)
|
# Try preferred DB first if provided
|
||||||
|
ordered_dbs = icdb_paths
|
||||||
for icdb_file in icdb_files:
|
if preferred_db is not None and preferred_db in icdb_paths:
|
||||||
try:
|
ordered_dbs = [preferred_db] + [p for p in icdb_paths if p != preferred_db]
|
||||||
icdb_path = os.path.join(icaros_cache_dir, icdb_file)
|
|
||||||
img = extract_from_icdb_database(icdb_path, file_path)
|
|
||||||
if img:
|
|
||||||
print(f" -> Found thumbnail in {icdb_file}")
|
|
||||||
return img
|
|
||||||
except Exception as e:
|
|
||||||
print(f" -> Failed to read {icdb_file}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
return img
|
||||||
|
print(f" -> Mapped index did not resolve a thumbnail in any DB")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -744,8 +724,259 @@ def extract_icaros_thumbnail(file_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_from_icdb_database(icdb_path, file_path):
|
# --- Icaros helpers: cache dir, index mapping, and database discovery ---
|
||||||
"""Extract thumbnail from Icaros .icdb binary cache file using alphabetical mapping"""
|
|
||||||
|
def _get_icaros_cache_dir():
|
||||||
|
"""Return the Icaros cache directory if found, else None."""
|
||||||
|
candidates = [
|
||||||
|
r"C:\\Program Files\\Icaros\\IcarosCache",
|
||||||
|
r"C:\\Program Files (x86)\\Icaros\\IcarosCache",
|
||||||
|
os.path.expanduser(r"~\\AppData\\Local\\Icaros\\IcarosCache"),
|
||||||
|
os.path.expanduser(r"~\\AppData\\Roaming\\Icaros\\IcarosCache"),
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_ICAROS_INDEX_CACHE = None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_windows_path(path):
|
||||||
|
try:
|
||||||
|
return os.path.normcase(os.path.abspath(path))
|
||||||
|
except Exception:
|
||||||
|
return path.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_icaros_index_map(cache_dir):
|
||||||
|
"""Attempt to build a file->position map from Icaros_idx.icdb (if SQLite).
|
||||||
|
|
||||||
|
Returns dict[path_lower] -> { 'index': int, 'db': optional full path to icdb }
|
||||||
|
"""
|
||||||
|
global _ICAROS_INDEX_CACHE
|
||||||
|
if _ICAROS_INDEX_CACHE is not None:
|
||||||
|
return _ICAROS_INDEX_CACHE
|
||||||
|
|
||||||
|
idx_path = os.path.join(cache_dir, 'Icaros_idx.icdb')
|
||||||
|
if not os.path.exists(idx_path):
|
||||||
|
_ICAROS_INDEX_CACHE = {}
|
||||||
|
return _ICAROS_INDEX_CACHE
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(idx_path)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Discover tables
|
||||||
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
tables = [r[0] for r in cur.fetchall()]
|
||||||
|
mapping = {}
|
||||||
|
|
||||||
|
def pick_cols(table):
|
||||||
|
cur.execute(f"PRAGMA table_info('{table}')")
|
||||||
|
cols = cur.fetchall()
|
||||||
|
# cols: cid, name, type, notnull, dflt_value, pk
|
||||||
|
path_cols = [c[1] for c in cols if isinstance(c[1], str) and c[1].lower() in (
|
||||||
|
'path', 'filepath', 'file_path', 'fullpath', 'filename', 'name')]
|
||||||
|
index_cols = [c[1] for c in cols if isinstance(c[1], str) and any(k in c[1].lower() for k in (
|
||||||
|
'index', 'position', 'pos', 'idx', 'imageindex', 'thumbindex'))]
|
||||||
|
db_cols = [c[1] for c in cols if isinstance(c[1], str) and any(k in c[1].lower() for k in (
|
||||||
|
'db', 'database', 'cache', 'size'))]
|
||||||
|
return path_cols, index_cols, db_cols
|
||||||
|
|
||||||
|
for t in tables:
|
||||||
|
try:
|
||||||
|
path_cols, index_cols, db_cols = pick_cols(t)
|
||||||
|
if not path_cols or not index_cols:
|
||||||
|
continue
|
||||||
|
# Use the first reasonable candidates
|
||||||
|
pcol = path_cols[0]
|
||||||
|
icol = index_cols[0]
|
||||||
|
dcol = db_cols[0] if db_cols else None
|
||||||
|
q = f"SELECT {pcol}, {icol}" + (f", {dcol}" if dcol else "") + f" FROM '{t}'"
|
||||||
|
for row in cur.execute(q):
|
||||||
|
try:
|
||||||
|
p = _normalize_windows_path(row[0])
|
||||||
|
idx_val = int(row[1]) if row[1] is not None else None
|
||||||
|
db_hint = None
|
||||||
|
if dcol and len(row) >= 3 and row[2]:
|
||||||
|
# Some schemas might store db size or name; try to resolve to a file
|
||||||
|
db_hint = _resolve_icdb_hint(cache_dir, str(row[2]))
|
||||||
|
if idx_val is not None:
|
||||||
|
mapping[p] = {'index': idx_val, 'db': db_hint}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
_ICAROS_INDEX_CACHE = mapping
|
||||||
|
if mapping:
|
||||||
|
print(f" -> Loaded Icaros index map for {len(mapping)} files")
|
||||||
|
else:
|
||||||
|
print(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}")
|
||||||
|
_ICAROS_INDEX_CACHE = {}
|
||||||
|
return _ICAROS_INDEX_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_icaros_index(cache_dir, file_path):
|
||||||
|
"""Return (index, preferred_db_path) for file_path by inspecting Icaros_idx.icdb.
|
||||||
|
|
||||||
|
Tries multiple strategies:
|
||||||
|
1) Direct SQLite mapping (exact normalized full path match)
|
||||||
|
2) If SQLite not available, try binary scan for embedded UTF-16/UTF-8 path followed by an int
|
||||||
|
3) If still not found, try UNC/drive-letter normalization variants (R:\ -> \\Server\Share)
|
||||||
|
Returns (None, None) if not found.
|
||||||
|
"""
|
||||||
|
# Try SQLite-based mapping first
|
||||||
|
index_map = _build_icaros_index_map(cache_dir)
|
||||||
|
key = _normalize_windows_path(file_path)
|
||||||
|
if key in index_map:
|
||||||
|
entry = index_map[key]
|
||||||
|
return entry.get('index'), entry.get('db')
|
||||||
|
|
||||||
|
# Try alternate path forms (UNC vs drive letter)
|
||||||
|
alt_keys = _generate_alternate_windows_paths(key)
|
||||||
|
for ak in alt_keys:
|
||||||
|
if ak in index_map:
|
||||||
|
entry = index_map[ak]
|
||||||
|
return entry.get('index'), entry.get('db')
|
||||||
|
|
||||||
|
# Binary fallback: attempt to find path occurrence in Icaros_idx.icdb and read nearby int
|
||||||
|
idx_path = os.path.join(cache_dir, 'Icaros_idx.icdb')
|
||||||
|
try:
|
||||||
|
with open(idx_path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
# Try UTF-16LE
|
||||||
|
needle_utf16 = key.encode('utf-16le')
|
||||||
|
pos = data.find(needle_utf16)
|
||||||
|
if pos == -1:
|
||||||
|
# Try UTF-8
|
||||||
|
needle_utf8 = key.encode('utf-8')
|
||||||
|
pos = data.find(needle_utf8)
|
||||||
|
if pos != -1:
|
||||||
|
# Scan forward a small window for a plausible 32-bit little-endian index
|
||||||
|
win = data[pos:pos + 256]
|
||||||
|
for off in range(0, min(256 - 4, len(win) - 4), 4):
|
||||||
|
try:
|
||||||
|
idx_candidate = struct.unpack('<I', win[off:off+4])[0]
|
||||||
|
if 0 <= idx_candidate < 100000: # sanity bound
|
||||||
|
return idx_candidate, None
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Environment-guided fallback: user-provided monitored roots (semi-colon separated)
|
||||||
|
# Example: ICAROS_MONITORED_ROOTS=R:\\YouTube\\Streams\\MixerTwitch;R:\\Videos
|
||||||
|
roots_env = os.environ.get('ICAROS_MONITORED_ROOTS')
|
||||||
|
if roots_env:
|
||||||
|
roots = [r.strip() for r in roots_env.split(';') if r.strip()]
|
||||||
|
idx = _compute_index_from_roots(roots, file_path)
|
||||||
|
if idx is not None:
|
||||||
|
return idx, None
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_alternate_windows_paths(norm_path):
|
||||||
|
"""Generate alternate path spellings (drive <-> UNC) to match Icaros records."""
|
||||||
|
variants = set()
|
||||||
|
p = norm_path
|
||||||
|
variants.add(p)
|
||||||
|
try:
|
||||||
|
# If path is drive form like R:\folder, try to map to UNC if the drive is a mapped network drive
|
||||||
|
if len(p) >= 3 and p[1:3] == ':\\':
|
||||||
|
drive = p[0:2]
|
||||||
|
# Environment-based hint (not perfect): if a share root env exists
|
||||||
|
unc_root = os.environ.get('ICAROS_UNC_ROOT') # e.g. \\Hydra\Hydra
|
||||||
|
if unc_root:
|
||||||
|
tail = p[2:].lstrip('\\')
|
||||||
|
variants.add(os.path.normcase(os.path.join(unc_root, tail)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return list(variants)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_index_from_roots(roots, file_path):
|
||||||
|
"""Approximate Icaros index by enumerating all supported files under provided roots
|
||||||
|
and sorting case-insensitively, then taking the ordinal of file_path within that list.
|
||||||
|
Returns None if file not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
supported_exts = {'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.m4v', '.ts', '.webm'}
|
||||||
|
all_files = []
|
||||||
|
norm_roots = []
|
||||||
|
for root in roots:
|
||||||
|
if not root:
|
||||||
|
continue
|
||||||
|
nr = os.path.normcase(os.path.abspath(root))
|
||||||
|
norm_roots.append(nr)
|
||||||
|
if os.path.exists(nr):
|
||||||
|
for r, dnames, fnames in os.walk(nr):
|
||||||
|
for fn in fnames:
|
||||||
|
ext = os.path.splitext(fn)[1].lower()
|
||||||
|
if ext in supported_exts:
|
||||||
|
all_files.append(os.path.join(r, fn))
|
||||||
|
if not all_files:
|
||||||
|
return None
|
||||||
|
all_files.sort(key=lambda p: os.path.normcase(p))
|
||||||
|
key = os.path.normcase(os.path.abspath(file_path))
|
||||||
|
try:
|
||||||
|
return all_files.index(key)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_icdb_hint(cache_dir, hint):
|
||||||
|
"""Try to resolve a DB hint (e.g., size or name) to a concrete .icdb file path."""
|
||||||
|
try:
|
||||||
|
paths = _list_icdb_databases(cache_dir)
|
||||||
|
# If hint is a number (like 2560), pick that size DB
|
||||||
|
try:
|
||||||
|
size = int(str(hint).strip())
|
||||||
|
for p in paths:
|
||||||
|
base = os.path.basename(p)
|
||||||
|
if f"_{size}.icdb" in base:
|
||||||
|
return p
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# If hint is a substring of a file name
|
||||||
|
for p in paths:
|
||||||
|
if str(hint).lower() in os.path.basename(p).lower():
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _list_icdb_databases(cache_dir):
|
||||||
|
"""Return .icdb database paths ordered by preferred resolution (largest first)."""
|
||||||
|
files = [f for f in os.listdir(cache_dir) if f.endswith('.icdb') and f.lower() != 'icaros_idx.icdb']
|
||||||
|
def get_size_from_filename(filename):
|
||||||
|
try:
|
||||||
|
if '_' in filename and filename.endswith('.icdb'):
|
||||||
|
return int(filename.split('_')[1].split('.')[0])
|
||||||
|
return 0
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return 0
|
||||||
|
files.sort(key=get_size_from_filename, reverse=True)
|
||||||
|
return [os.path.join(cache_dir, f) for f in files]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_from_icdb_database(icdb_path, file_path, forced_index=None):
|
||||||
|
"""Extract thumbnail from Icaros .icdb binary cache file.
|
||||||
|
|
||||||
|
If forced_index is provided, use that exact JPEG ordinal if present; otherwise
|
||||||
|
fall back to heuristic alphabetical mapping (best-effort).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
import struct
|
import struct
|
||||||
import glob
|
import glob
|
||||||
@@ -776,7 +1007,27 @@ def extract_from_icdb_database(icdb_path, file_path):
|
|||||||
|
|
||||||
print(f" -> Found {len(jpeg_positions)} JPEG images in {icdb_path}")
|
print(f" -> Found {len(jpeg_positions)} JPEG images in {icdb_path}")
|
||||||
|
|
||||||
# Use discovered alphabetical algorithm
|
# If we have a mapped index, try it directly first
|
||||||
|
if isinstance(forced_index, int) and len(jpeg_positions) > 0:
|
||||||
|
pos_index = forced_index
|
||||||
|
if pos_index < 0:
|
||||||
|
pos_index = 0
|
||||||
|
if pos_index >= len(jpeg_positions):
|
||||||
|
pos_index = pos_index % len(jpeg_positions)
|
||||||
|
jpeg_start = jpeg_positions[pos_index]
|
||||||
|
jpeg_end = data.find(b'\xff\xd9', jpeg_start)
|
||||||
|
if jpeg_end != -1:
|
||||||
|
jpeg_end += 2
|
||||||
|
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)}")
|
||||||
|
return img.copy()
|
||||||
|
except Exception:
|
||||||
|
# If mapped position invalid, continue to heuristic
|
||||||
|
print(f" -> Mapped index {pos_index} invalid; falling back to heuristic")
|
||||||
|
|
||||||
|
# Heuristic alphabetical mapping (fallback)
|
||||||
def get_alphabetical_position(target_file_path):
|
def get_alphabetical_position(target_file_path):
|
||||||
"""Get cache position based on alphabetical sorting of monitored files"""
|
"""Get cache position based on alphabetical sorting of monitored files"""
|
||||||
# Get Icaros supported extensions
|
# Get Icaros supported extensions
|
||||||
|
|||||||
Reference in New Issue
Block a user