Files
claude-regelwerk/.claude/skills/bookstack-upload/scripts/bookstack/bilder.py
T
Pascal MichelandClaude Opus 5 eb48f8822d Automatischer Push: 45 Datei(en) geändert
- .claude/agents/planer.md
- .claude/agents/quorum-stimme.md
- .claude/skills/bookstack-upload/SKILL.md
- .claude/skills/bookstack-upload/scripts/bookstack/archiv.py
- .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py
- .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py
- .claude/skills/bookstack-upload/scripts/bookstack/bilder.py
- .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py
- .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py
- .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py
- .claude/skills/bookstack-upload/scripts/bookstack/struktur.py
- .claude/skills/bookstack-upload/scripts/bookstack/verweise.py
- … und 33 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 04:00:02 +02:00

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