.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
126 lines
4.6 KiB
Python
126 lines
4.6 KiB
Python
"""Findet Bilder im Vault und lädt sie in die BookStack-Bildergalerie.
|
|
|
|
Ersteller: Pascal Michel
|
|
Erstellt: 2026-08-05
|
|
Doku: docs/code/bilder.md · Version und Änderungen: CHANGELOG.md
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from bookstack.api import ApiFehler, BookStackApi
|
|
|
|
# In D:\Claude\skills\ liegt ein grosses Fremd-Repo — laut CLAUDE.md haengt sich
|
|
# ein rekursiver Lauf dort praktisch endlos auf. Diese Ordner werden uebersprungen.
|
|
UEBERSPRINGEN = {"skills", ".git", ".venv", "node_modules", ".obsidian", "__pycache__", ".pytest_cache"}
|
|
|
|
TYPEN = {
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".gif": "image/gif",
|
|
".webp": "image/webp",
|
|
".avif": "image/avif",
|
|
}
|
|
# Die API-Doku nennt „max 50KB"; das ist mit hoher Wahrscheinlichkeit die
|
|
# Laravel-Regel max:50000, die bei Dateien in Kilobyte rechnet (~48,8 MB).
|
|
# Deshalb hier eine Grenze knapp darunter — die echte Ablehnung der API wird
|
|
# ohnehin woertlich weitergereicht.
|
|
MAX_BYTES = 48 * 1024 * 1024
|
|
|
|
|
|
class BildFehler(Exception):
|
|
"""Ein Bild konnte nicht übertragen werden."""
|
|
|
|
|
|
def finde_bilddatei(vault: Path, notiz_pfad: Path, name: str) -> Path | None:
|
|
"""Sucht erst neben der Notiz, dann in der Vault-Wurzel, dann im ganzen Vault."""
|
|
for kandidat in (notiz_pfad.parent / name, vault / name):
|
|
if kandidat.is_file():
|
|
return kandidat
|
|
|
|
for wurzel, ordner, dateien in os.walk(vault):
|
|
ordner[:] = [eintrag for eintrag in ordner if eintrag not in UEBERSPRINGEN]
|
|
if name in dateien:
|
|
return Path(wurzel) / name
|
|
return None
|
|
|
|
|
|
class Bilduploader:
|
|
def __init__(self, api: BookStackApi, vault: Path) -> None:
|
|
self._api = api
|
|
self._vault = vault
|
|
|
|
def _lies_galerie(self, seiten_id: int) -> dict[str, str]:
|
|
"""Bereits an dieser Seite hängende Galeriebilder, Name → URL; ApiFehler geht durch."""
|
|
eintraege = self._api.liste(f"/api/image-gallery?filter[uploaded_to]={seiten_id}")
|
|
return {str(eintrag.get("name", "")): str(eintrag.get("url", "")) for eintrag in eintraege}
|
|
|
|
def _vorhandene(self, seiten_id: int) -> dict[str, str]:
|
|
"""Wie _lies_galerie, aber ein ApiFehler ergibt {}; nur für lade_hoch."""
|
|
try:
|
|
return self._lies_galerie(seiten_id)
|
|
except ApiFehler:
|
|
return {}
|
|
|
|
def vorhandene(self, seiten_id: int) -> dict[str, str]:
|
|
"""Galeriebilder der Seite, Name → URL; nur lesend, für die Verweisrunde.
|
|
|
|
Ein ApiFehler geht durch: Ein leeres Ergebnis sähe dort aus wie ein fehlendes Bild.
|
|
"""
|
|
return self._lies_galerie(seiten_id)
|
|
|
|
def lade_hoch(
|
|
self, notiz_pfad: Path, seiten_id: int, namen: list[str]
|
|
) -> tuple[dict[str, str], list[str]]:
|
|
"""Liefert (Name → URL) und eine Liste von Hinweisen für den Bericht."""
|
|
tabelle: dict[str, str] = {}
|
|
hinweise: list[str] = []
|
|
if not namen:
|
|
return tabelle, hinweise
|
|
|
|
# Ein Lesefehler der Galerie ergibt hier {}, gewollt: Das Bild wird neu hochgeladen, statt den
|
|
# Upload scheitern zu lassen, und kostet höchstens ein Doppelbild. Die Verweisrunde liest über
|
|
# vorhandene, dort geht der Fehler durch.
|
|
vorhanden = self._vorhandene(seiten_id)
|
|
|
|
for name in dict.fromkeys(namen):
|
|
if name in vorhanden and vorhanden[name]:
|
|
tabelle[name] = vorhanden[name]
|
|
continue
|
|
|
|
pfad = finde_bilddatei(self._vault, notiz_pfad, name)
|
|
if pfad is None:
|
|
hinweise.append(f"Bild nicht gefunden im Vault: {name}")
|
|
continue
|
|
|
|
mime = TYPEN.get(pfad.suffix.lower())
|
|
if mime is None:
|
|
hinweise.append(f"Bildtyp nicht unterstützt: {name}")
|
|
continue
|
|
|
|
groesse = pfad.stat().st_size
|
|
if groesse > MAX_BYTES:
|
|
hinweise.append(f"Bild zu groß ({groesse // (1024 * 1024)} MB): {name}")
|
|
continue
|
|
|
|
try:
|
|
antwort = self._api.sende_formular(
|
|
"/api/image-gallery",
|
|
{"type": "gallery", "uploaded_to": str(seiten_id), "name": name},
|
|
("image", name, pfad.read_bytes(), mime),
|
|
)
|
|
except ApiFehler as fehler:
|
|
hinweise.append(f"Bild abgelehnt ({name}): {fehler.meldung}")
|
|
continue
|
|
|
|
url = str(antwort.get("url", ""))
|
|
if url:
|
|
tabelle[name] = url
|
|
else:
|
|
hinweise.append(f"Bild ohne URL zurückgekommen: {name}")
|
|
|
|
return tabelle, hinweise
|