"""BAN YARO — Tagebuch Routes""" import os, uuid, json from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user import ki as KI router = APIRouter() MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") class DiaryCreate(BaseModel): datum: Optional[str] = None # ISO date, default heute typ: str = "eintrag" titel: Optional[str] = None text: Optional[str] = None tags: Optional[list] = None gps_lat: Optional[float] = None gps_lon: Optional[float] = None is_milestone: bool = False dog_ids: Optional[list[int]] = None # alle Hunde inkl. primär; None = nur primary class DiaryUpdate(BaseModel): titel: Optional[str] = None text: Optional[str] = None tags: Optional[list] = None is_milestone: Optional[bool] = None dog_ids: Optional[list[int]] = None # wenn gesetzt: Hunde-Zuweisung ersetzen def _own_dog(dog_id: int, user_id: int, conn): dog = conn.execute( "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") return dog def _validate_dog_ids(dog_ids: list[int], primary: int, user_id: int, conn) -> list[int]: """Stellt sicher dass alle IDs dem User gehören. Gibt die bereinigte Liste zurück.""" all_ids = list({primary} | set(dog_ids)) for did in all_ids: _own_dog(did, user_id, conn) return all_ids def _fetch_dog_ids(conn, entry_ids: list[int]) -> dict: """Gibt {entry_id: [dog_id, ...]} zurück.""" if not entry_ids: return {} ph = ",".join("?" * len(entry_ids)) rows = conn.execute( f"SELECT diary_id, dog_id FROM diary_dogs WHERE diary_id IN ({ph})", entry_ids ).fetchall() result = {} for r in rows: result.setdefault(r["diary_id"], []).append(r["dog_id"]) return result def _set_dog_ids(conn, entry_id: int, dog_ids: list[int]): conn.execute("DELETE FROM diary_dogs WHERE diary_id=?", (entry_id,)) for did in dog_ids: conn.execute( "INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) VALUES (?,?)", (entry_id, did) ) def _entry_dict(row, dog_ids_map: dict) -> dict: e = dict(row) e["tags"] = json.loads(e["tags"]) if e["tags"] else [] e["dog_ids"] = dog_ids_map.get(e["id"], [e["dog_id"]]) return e @router.get("/{dog_id}/diary") async def list_diary(dog_id: int, limit: int = 20, offset: int = 0, user=Depends(get_current_user)): with db() as conn: _own_dog(dog_id, user["id"], conn) # Einträge des primären Hundes SOWIE Einträge wo der Hund als weiterer zugeordnet ist rows = conn.execute( """SELECT DISTINCT d.* FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.dog_id = ? OR dd.dog_id = ? ORDER BY d.datum DESC, d.created_at DESC LIMIT ? OFFSET ?""", (dog_id, dog_id, limit, offset) ).fetchall() ids = [r["id"] for r in rows] dogs_map = _fetch_dog_ids(conn, ids) return [_entry_dict(r, dogs_map) for r in rows] @router.post("/{dog_id}/diary", status_code=201) async def create_diary(dog_id: int, data: DiaryCreate, user=Depends(get_current_user)): tags = data.tags or [] # KI: Auto-Tags wenn Text vorhanden (lokal, kostenlos) if data.text and len(data.text) > 10: try: ai_tags = await KI.diary_tags(data.text) tags = list(set(tags + ai_tags)) except Exception: pass with db() as conn: _own_dog(dog_id, user["id"], conn) all_dogs = _validate_dog_ids(data.dog_ids or [], dog_id, user["id"], conn) conn.execute( """INSERT INTO diary (dog_id, datum, typ, titel, text, tags, gps_lat, gps_lon, is_milestone) VALUES (?, COALESCE(?, date('now')), ?,?,?,?,?,?,?)""", (dog_id, data.datum, data.typ, data.titel, data.text, json.dumps(tags), data.gps_lat, data.gps_lon, int(data.is_milestone)) ) entry = conn.execute( "SELECT * FROM diary WHERE dog_id=? ORDER BY id DESC LIMIT 1", (dog_id,) ).fetchone() _set_dog_ids(conn, entry["id"], all_dogs) dogs_map = _fetch_dog_ids(conn, [entry["id"]]) return _entry_dict(entry, dogs_map) @router.get("/{dog_id}/diary/{entry_id}") async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)): with db() as conn: _own_dog(dog_id, user["id"], conn) row = conn.execute( """SELECT DISTINCT d.* FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""", (entry_id, dog_id, dog_id) ).fetchone() if not row: raise HTTPException(404, "Eintrag nicht gefunden.") dogs_map = _fetch_dog_ids(conn, [entry_id]) return _entry_dict(row, dogs_map) @router.patch("/{dog_id}/diary/{entry_id}") async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate, user=Depends(get_current_user)): with db() as conn: _own_dog(dog_id, user["id"], conn) # Prüfen ob Eintrag diesem Hund gehört (direkt oder via diary_dogs) exists = conn.execute( """SELECT 1 FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""", (entry_id, dog_id, dog_id) ).fetchone() if not exists: raise HTTPException(404, "Eintrag nicht gefunden.") # Felder updaten fields = {k: v for k, v in data.model_dump(exclude={"dog_ids"}).items() if v is not None} if "tags" in fields: fields["tags"] = json.dumps(fields["tags"]) if fields: # primary dog_id für SET-Clause ermitteln (der Eintrag bleibt dem Erstell-Hund) set_clause = ", ".join(f"{k}=?" for k in fields) conn.execute( f"UPDATE diary SET {set_clause} WHERE id=?", list(fields.values()) + [entry_id] ) # Hunde-Zuweisung aktualisieren if data.dog_ids is not None: # primary dog des Eintrags ermitteln primary = conn.execute( "SELECT dog_id FROM diary WHERE id=?", (entry_id,) ).fetchone()["dog_id"] all_dogs = _validate_dog_ids(data.dog_ids, primary, user["id"], conn) _set_dog_ids(conn, entry_id, all_dogs) row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() dogs_map = _fetch_dog_ids(conn, [entry_id]) return _entry_dict(row, dogs_map) @router.delete("/{dog_id}/diary/{entry_id}", status_code=204) async def delete_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)): with db() as conn: _own_dog(dog_id, user["id"], conn) conn.execute( "DELETE FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id) ) @router.post("/{dog_id}/diary/{entry_id}/media") async def upload_media(dog_id: int, entry_id: int, file: UploadFile = File(...), user=Depends(get_current_user)): with db() as conn: _own_dog(dog_id, user["id"], conn) entry = conn.execute( """SELECT d.id FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""", (entry_id, dog_id, dog_id) ).fetchone() if not entry: raise HTTPException(404, "Eintrag nicht gefunden.") ext = os.path.splitext(file.filename or "")[1] or ".jpg" filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}" path = os.path.join(MEDIA_DIR, "diary", filename) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(await file.read()) media_url = f"/media/diary/{filename}" with db() as conn: conn.execute("UPDATE diary SET media_url=? WHERE id=?", (media_url, entry_id)) return {"media_url": media_url}