Complete photo deletion on disk, snappy lightbox, keyboard shortcuts, prefetching, and perfected documentation

This commit is contained in:
drjones
2026-07-20 21:08:07 -07:00
parent 5208c5ecef
commit c07fb9d971
3 changed files with 286 additions and 90 deletions

121
server.py
View File

@@ -242,6 +242,101 @@ def journal_write(rec):
with open(JOURNAL, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
def trash_or_remove_file(path):
if not os.path.exists(path):
return True
try:
# Try macOS Trash via AppleScript
cmd = ["osascript", "-e", f'tell application "Finder" to delete POSIX file "{os.path.abspath(path)}"']
r = subprocess.run(cmd, capture_output=True, timeout=10)
if r.returncode == 0:
return True
except Exception:
pass
try:
os.remove(path)
return True
except Exception as e:
log("error", f"Failed to delete file {path}: {e}")
return False
def remove_photo_records(path):
abs_path = os.path.abspath(path)
# 1. Remove symlink in _organized
with S.lock:
folder = S.folder
org_root = os.path.join(folder, "_organized")
if os.path.exists(org_root):
for root, dirs, files in os.walk(org_root):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for name in files:
p = os.path.join(root, name)
if os.path.islink(p):
try:
if os.path.realpath(p) == abs_path:
os.remove(p)
except Exception:
pass
# 2. Clean thumbnail and view caches
try:
h = hashlib.md5(abs_path.encode()).hexdigest()
t1 = os.path.join(tempfile.gettempdir(), "photon_thumbs", f"{h}.jpg")
t2 = os.path.join(tempfile.gettempdir(), "photon_view", f"{h}.jpg")
for t in (t1, t2):
if os.path.exists(t):
try: os.remove(t)
except Exception: pass
except Exception:
pass
# 3. Rewrite journal without this path
if os.path.exists(JOURNAL):
lines_to_keep = []
with open(JOURNAL, "r", encoding="utf-8") as f:
for line in f:
line_str = line.strip()
if not line_str:
continue
try:
rec = json.loads(line_str)
if os.path.abspath(rec.get("path", "")) != abs_path:
lines_to_keep.append(line_str)
except Exception:
lines_to_keep.append(line_str)
with open(JOURNAL, "w", encoding="utf-8") as f:
for l in lines_to_keep:
f.write(l + "\n")
# 4. Remove from State
with S.lock:
if abs_path in S.done_paths:
S.done_paths.remove(abs_path)
if abs_path in S.files:
S.files.remove(abs_path)
if abs_path in S.video_files:
S.video_files.remove(abs_path)
if abs_path in S.failed_paths:
S.failed_paths.remove(abs_path)
write_failures()
global SEARCH_CACHE
SEARCH_CACHE = {"mtime": None, "records": []}
load_journal()
push_stats()
def delete_photo_file_and_record(path):
if not path:
return False, "path is required"
abs_path = os.path.abspath(path)
deleted_disk = False
if os.path.exists(abs_path):
deleted_disk = trash_or_remove_file(abs_path)
else:
deleted_disk = True
remove_photo_records(abs_path)
return deleted_disk, None
SEARCH_CACHE = {"mtime": None, "records": []}
def load_search_index():
@@ -1864,6 +1959,32 @@ class Handler(BaseHTTPRequestHandler):
except Exception as e:
self._json({"error": str(e)}, 500)
elif self.path == "/api/delete_photo":
path = body.get("path")
if not path:
self._json({"error": "path is required"}, 400); return
success, err = delete_photo_file_and_record(path)
if success:
log("ok", f"Deleted photo from disk: {os.path.basename(path)}")
self._json({"ok": True, "path": path})
else:
self._json({"error": f"Failed deleting file: {err}"}, 500)
elif self.path == "/api/delete_photos":
paths = body.get("paths") or []
if not paths:
self._json({"error": "paths array is required"}, 400); return
deleted_count = 0
failed = []
for p in paths:
success, err = delete_photo_file_and_record(p)
if success:
deleted_count += 1
else:
failed.append({"path": p, "error": err})
log("ok", f"Batch deleted {deleted_count} photos from disk" + (f" ({len(failed)} failed)" if failed else ""))
self._json({"ok": True, "deleted": deleted_count, "failed": failed})
elif self.path == "/api/dedupe/build":
with S.lock:
already_running = S.dedupe_status == "running"