Add shared favorite location v0.4.2

This commit is contained in:
dmo committed 2026-07-23 09:54:25 +02:00
1 parent 9b7d302064
commit e250a83b6d
8 files changed
+286 -9

No files matched your search

+89 -1
View File
@@ -7,7 +7,9 @@ import os
import re
import subprocess
import tempfile
import threading
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request
@@ -17,12 +19,13 @@ from pathlib import Path
from typing import Annotated, Any
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from pydantic import BaseModel, Field
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles
APP_NAME = os.getenv("APP_NAME", "Photo Date Editor")
APP_URL = os.getenv("APP_URL", "http://localhost:8080")
APP_VERSION = "0.4.1"
APP_VERSION = "0.4.2"
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "150"))
MAX_UPLOAD_BYTES = MAX_UPLOAD_MB * 1024 * 1024
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
@@ -59,6 +62,16 @@ logger = logging.getLogger("photo-date-editor")
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static"
DATA_DIR = Path(os.getenv("DATA_DIR", "/data"))
FAVORITES_FILE = DATA_DIR / "favorite-locations.json"
_favorites_lock = threading.Lock()
class FavoriteLocationInput(BaseModel):
name: str = Field(min_length=1, max_length=120)
latitude: float = Field(ge=-90, le=90)
longitude: float = Field(ge=-180, le=180)
app = FastAPI(title=APP_NAME, docs_url=None, redoc_url=None)
@@ -83,6 +96,81 @@ def config() -> dict[str, str | int | float]:
}
def _load_favorites_unlocked() -> list[dict[str, Any]]:
if not FAVORITES_FILE.exists():
return []
try:
payload = json.loads(FAVORITES_FILE.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
logger.warning("Could not read favorite locations: %s", exc)
return []
raw = payload.get("favorites", []) if isinstance(payload, dict) else []
favorites: list[dict[str, Any]] = []
for item in raw if isinstance(raw, list) else []:
if not isinstance(item, dict):
continue
try:
latitude = float(item["latitude"])
longitude = float(item["longitude"])
name = str(item["name"]).strip()
favorite_id = str(item["id"]).strip()
except (KeyError, TypeError, ValueError):
continue
if not name or not favorite_id or not -90 <= latitude <= 90 or not -180 <= longitude <= 180:
continue
favorites.append({
"id": favorite_id,
"name": name,
"latitude": latitude,
"longitude": longitude,
})
return sorted(favorites, key=lambda item: item["name"].casefold())
def _write_favorites_unlocked(favorites: list[dict[str, Any]]) -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
payload = {"favorites": sorted(favorites, key=lambda item: item["name"].casefold())}
temporary = FAVORITES_FILE.with_suffix(".tmp")
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(FAVORITES_FILE)
@app.get("/api/favorites")
def list_favorites() -> dict[str, list[dict[str, Any]]]:
with _favorites_lock:
return {"favorites": _load_favorites_unlocked()}
@app.post("/api/favorites", status_code=201)
def create_favorite(value: FavoriteLocationInput) -> dict[str, dict[str, Any]]:
name = " ".join(value.name.split())
if not name:
raise HTTPException(status_code=422, detail="Favorite name cannot be empty.")
with _favorites_lock:
favorites = _load_favorites_unlocked()
if any(item["name"].casefold() == name.casefold() for item in favorites):
raise HTTPException(status_code=409, detail="A favorite with that name already exists.")
favorite = {
"id": uuid.uuid4().hex,
"name": name,
"latitude": round(value.latitude, 6),
"longitude": round(value.longitude, 6),
}
favorites.append(favorite)
_write_favorites_unlocked(favorites)
return {"favorite": favorite}
@app.delete("/api/favorites/{favorite_id}", status_code=204)
def delete_favorite(favorite_id: str) -> Response:
with _favorites_lock:
favorites = _load_favorites_unlocked()
remaining = [item for item in favorites if item["id"] != favorite_id]
if len(remaining) == len(favorites):
raise HTTPException(status_code=404, detail="Favorite location not found.")
_write_favorites_unlocked(remaining)
return Response(status_code=204)
def _fetch_map_tile(z: int, x: int, y: int) -> tuple[bytes, str]:
tile_url = (