Files
OnlyFrames/server.py
Ferdinand Urban 9f44b8c4f2 chore: CLAUDE.md compliance — relative paths, CORS, README tech stack, .vch-description
- Fix absolute API paths in index.html (/analyze, /move, /preview → relative)
- Allow all CORS origins in server.py for reverse-proxy compatibility
- Add tech stack section to README.md
- Create .vch-description for VCH Showcase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:13:43 +00:00

130 lines
3.8 KiB
Python

import os
import shutil
import webbrowser
import threading
from typing import List
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, Response
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
from analyzer import analyze_folder
load_dotenv()
app = FastAPI(title="Foto-Kurator")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST"],
allow_headers=["Content-Type"],
)
class AnalyzeRequest(BaseModel):
folder: str
blur_threshold: float = 100.0
over_threshold: float = 240.0
under_threshold: float = 30.0
dup_threshold: int = 8
use_ai: bool = False
class MoveRequest(BaseModel):
paths: List[str]
folder: str
@app.get("/")
def serve_frontend():
return FileResponse("index.html")
@app.get("/pick-folder")
def pick_folder():
import subprocess
result = subprocess.run(
["osascript", "-e", 'POSIX path of (choose folder with prompt "Ordner auswählen")'],
capture_output=True, text=True
)
folder = result.stdout.strip().rstrip("/")
if not folder:
raise HTTPException(status_code=204, detail="Kein Ordner ausgewählt")
return {"folder": folder}
PREVIEW_ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"}
@app.get("/preview")
def preview(path: str):
ext = os.path.splitext(path)[1].lower()
if ext not in PREVIEW_ALLOWED_EXTENSIONS:
raise HTTPException(status_code=403, detail="Dateityp nicht erlaubt")
if not os.path.isfile(path):
raise HTTPException(status_code=404, detail="Datei nicht gefunden")
media = "image/jpeg" if ext in (".jpg", ".jpeg") else "image/png"
with open(path, "rb") as f:
return Response(content=f.read(), media_type=media)
@app.post("/analyze")
def analyze(req: AnalyzeRequest):
if not os.path.isdir(req.folder):
raise HTTPException(status_code=400, detail=f"Ordner nicht gefunden: {req.folder}")
api_key = os.getenv("ANTHROPIC_API_KEY") if req.use_ai else None
results = analyze_folder(
folder=req.folder,
blur_threshold=req.blur_threshold,
over_threshold=req.over_threshold,
under_threshold=req.under_threshold,
dup_threshold=req.dup_threshold,
use_ai=req.use_ai,
api_key=api_key,
)
from analyzer import SUPPORTED_EXTENSIONS
all_paths = {
os.path.join(req.folder, f)
for f in os.listdir(req.folder)
if os.path.splitext(f)[1].lower() in SUPPORTED_EXTENSIONS
}
flagged_paths = {item["path"] for item in results}
ok_paths = sorted(all_paths - flagged_paths)
return {"results": results, "ok_paths": ok_paths}
@app.post("/move")
def move_files(req: MoveRequest):
folder_abs = os.path.abspath(req.folder)
if not os.path.isdir(folder_abs):
raise HTTPException(status_code=400, detail=f"Ordner nicht gefunden: {req.folder}")
target_dir = os.path.join(folder_abs, "_aussortiert")
os.makedirs(target_dir, exist_ok=True)
moved = []
errors = []
for path in req.paths:
path_abs = os.path.abspath(path)
if not path_abs.startswith(folder_abs + os.sep):
errors.append({"path": path, "error": "Pfad liegt außerhalb des analysierten Ordners"})
continue
try:
dest = os.path.join(target_dir, os.path.basename(path_abs))
shutil.move(path_abs, dest)
moved.append(path)
except Exception as e:
errors.append({"path": path, "error": str(e)})
return {"moved": moved, "errors": errors}
def open_browser():
webbrowser.open("http://localhost:8000")
if __name__ == "__main__":
threading.Timer(1.0, open_browser).start()
uvicorn.run(app, host="127.0.0.1", port=8000)