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=["http://localhost:8000"], 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)