Add self-healing path relocation for multi-disk portability, macOS native folder picker, and double-clickable launch.command
This commit is contained in:
15
index.html
15
index.html
@@ -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
20
launch.command
Executable 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
|
||||
40
server.py
40
server.py
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user