Add self-healing path relocation for multi-disk portability, macOS native folder picker, and double-clickable launch.command

This commit is contained in:
drjones
2026-07-20 21:18:12 -07:00
parent 82e93972c1
commit 2d3ef6a06c
3 changed files with 68 additions and 7 deletions

View File

@@ -341,6 +341,7 @@ mark{background:rgba(34,211,238,.28);color:#fff;border-radius:2px;padding:0 1px}
<div class="controls">
<div class="folderRow">
<input id="folder" spellcheck="false" placeholder="/path/to/your/photos">
<button id="btnBrowseFolder" title="Open native macOS folder chooser">📁 Browse</button>
<button id="btnScan">◈ Scan Directory</button>
</div>
<button id="btnStart" class="btn-primary" disabled>▶ Start Pipeline</button>
@@ -991,6 +992,20 @@ async function api(path, body){
return j;
}
if($("btnBrowseFolder")){
$("btnBrowseFolder").onclick = async ()=>{
$("btnBrowseFolder").disabled = true;
const j = await api("/api/choose_folder");
$("btnBrowseFolder").disabled = false;
if(j.ok && j.folder){
$("folder").value = j.folder;
showToast("Selected folder: " + j.folder, "ok");
const scanRes = await api("/api/scan",{folder: j.folder});
if(scanRes.images!=null){ scanned=true; setBadge("idle"); }
}
};
}
$("btnScan").onclick = async ()=>{
const j = await api("/api/scan",{folder:$("folder").value});
if(j.images!=null){ scanned=true; setBadge("idle"); }

20
launch.command Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# PHOTON — Double-click macOS launcher
# Automatically starts the server and opens the console in your default web browser.
CD_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$CD_DIR"
echo "======================================================="
echo " PHOTON — Local Photo Intelligence Console Launch "
echo "======================================================="
echo ""
echo "Starting local HTTP server..."
python3 server.py &
SERVER_PID=$!
sleep 1.5
echo "Opening browser at http://localhost:8765 ..."
open "http://localhost:8765"
wait $SERVER_PID

View File

@@ -214,6 +214,19 @@ def write_failures():
except Exception:
pass
def resolve_portable_path(path):
if not path:
return path
if os.path.exists(path):
return path
with S.lock:
curr_folder = S.folder
if curr_folder and os.path.isdir(curr_folder):
cand = os.path.join(curr_folder, os.path.basename(path))
if os.path.exists(cand):
return cand
return path
def load_journal():
n = 0
with S.lock:
@@ -227,8 +240,9 @@ def load_journal():
continue
try:
rec = json.loads(line)
path = resolve_portable_path(rec.get("path", ""))
with S.lock:
S.done_paths.add(rec["path"])
S.done_paths.add(path)
cat = rec.get("category")
if cat not in S.cat_counts:
S.cat_counts[cat] = 0
@@ -246,7 +260,6 @@ 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:
@@ -262,7 +275,6 @@ def trash_or_remove_file(path):
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")
@@ -278,7 +290,6 @@ def remove_photo_records(path):
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")
@@ -290,7 +301,6 @@ def remove_photo_records(path):
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:
@@ -308,7 +318,6 @@ def remove_photo_records(path):
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)
@@ -356,7 +365,8 @@ def load_search_index():
continue
try:
rec = json.loads(line)
path = rec.get("path", "")
path = resolve_portable_path(rec.get("path", ""))
rec["path"] = path
name = os.path.basename(path)
rec["name"] = name
rec["_ext"] = os.path.splitext(name)[1].lower()
@@ -1678,6 +1688,22 @@ class Handler(BaseHTTPRequestHandler):
self._json({"images": len(images), "videos": len(videos),
"sidecars": sidecars, "alreadyDone": done})
elif self.path == "/api/choose_folder":
try:
cmd = ["osascript", "-e", 'POSIX path of (choose folder with prompt "Select Photo Folder to Organize:")']
r = subprocess.run(cmd, capture_output=True, timeout=60)
if r.returncode == 0:
chosen = r.stdout.decode("utf-8").strip()
if chosen and os.path.isdir(chosen):
with S.lock:
S.folder = chosen
save_folder_config(chosen)
self._json({"ok": True, "folder": chosen})
return
self._json({"error": "No folder selected"}, 400)
except Exception as e:
self._json({"error": str(e)}, 500)
elif self.path == "/api/start":
with S.lock:
if S.status == "running":