Add shared favorite location v0.4.2
This commit is contained in:
1 parent
9b7d302064
commit
e250a83b6d
8 files changed
+286
-9
No files matched your search
+89
-1
@@ -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 = (
|
||||
|
||||
Reference in new issue
Block a user