Metadata-Version: 2.4
Name: kurmann-familienfilm-manager
Version: 1.3.0
Summary: CLI-Tool und Python-Bibliothek zur Veröffentlichung von Familienfilmen: Medienset-Erstellung, Infuse-Deployment, Webserver-Deployment und Archivierung.
License: MIT License
        
        Copyright (c) 2026 Patrick Kurmann
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/kurmann/familienfilm-manager
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: kurmann-mediaset-creator>=3.1.0
Requires-Dist: kurmann-schnittprojekt-leser>=0.3.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# familienfilm-manager

CLI-Tool und Python-Bibliothek zur Veröffentlichung von selbst geschnittenen Familienfilmen.
Orchestriert den gesamten Workflow: Metadaten extrahieren, Medienset erstellen (via `mediaset-creator`),
Infuse-/Webserver-Deployment und Archivierung.

## Geltungsbereich

Der Manager arbeitet auf **fertig geschnittenen Videos** – konkret den Export-Dateien
(`.mov`/`.mp4`/`.m4v`) aus iMovie, LumaFusion, Final Cut Pro etc. Originalaufnahmen
(also direkt vom iPhone, von der Kamera-SSD oder anderen Sensoren) sind **kein**
Eingang dieses Tools – die kommen anderswo hin (z. B. im
[`kamera-einleser`](https://github.com/kurmann/kamera-einleser)).

Praktische Konsequenz für die **Aufnahmedatum-Ermittlung**: Container-Tags wie
`creation_time` oder `com.apple.quicktime.creationdate` werden **nicht** als
Aufnahmedatum gewertet, weil sie bei Schnitt-Exporten systematisch das
**Render-Datum** widerspiegeln (also wann der Schnitt fertig wurde),
nicht die ursprüngliche Aufnahme. Stattdessen kommt das Aufnahmedatum
aus expliziten Quellen:

1. **CLI** (`--date 2024-08-14`)
2. **Dateiname** (`YYYY-MM-DD`-Präfix)
3. **Sidecar-Cache** (Idempotenz aus früherem Lookup)
4. **Schnittprojekt-Lookup** (das ursprüngliche Aufnahmedatum aus FCPXML/iMovieMobile, v1.3.0+)
5. Sonst: **ignoriert** (mit aktivem Lookup-Setup) oder **harter Fehler** (ohne)

Details in [Aufnahmedatum-Hierarchie (v1.3.0+)](#aufnahmedatum-hierarchie-v130).

## Inhalt

**Erste Schritte**
- [Voraussetzungen](#voraussetzungen) · [Installation](#installation) · [Verwendung (CLI)](#verwendung-cli)

**Workflows**
- [Verzeichnis-Publikation (Batch)](#verzeichnis-publikation-batch) — `publish-dir`
- [Verzeichnis-Überwachung](#verzeichnis-überwachung-watch-dir) — `watch-dir`
- [rclone-Quellen](#rclone-quellen) — Cloud/NAS direkt als Quelle

**Konzepte**
- [Web-Speicher-Budget](#web-speicher-budget) — Storage-Limit + Aufbewahrungsdauer
- [KI-Vorschaubild-Wahl & Persistierung](#ki-vorschaubild-wahl--persistierung) — Claude-Frame-Auswahl + Sidecar-Cache
- [Poison-Video-Detection](#poison-video-detection) — Auto-Skip kaputter Videos
- [Metadaten-Extraktion](#metadaten-extraktion) — Dateiname / QuickTime-Tags
- [Settings-Sidecar-Datei](#settings-sidecar-datei) — `.settings.toml` neben jedem Video
- [Verzeichnis-Settings](#verzeichnis-settings-familienfilmtoml) — `familienfilm.toml` für Defaults

**Konfiguration**
- [API-Keys (KI)](#api-keys-ki-funktionen) · [Konfiguration](#konfiguration) · [Python API](#verwendung-python-api)

---

## Quickstart

Drei Schritte vom Null zur ersten Publikation:

```bash
# 1. Installieren
uv tool install kurmann-familienfilm-manager

# 2. Konfigurieren (rclone-Remotes für Targets sind Pflicht)
familienfilm-manager config set targets.infuse  "nas:/Familienfilme/Final"
familienfilm-manager config set targets.archive "nas:/Archiv/2026/Videoschnitt"
familienfilm-manager config set targets.web     "hostpoint:"
familienfilm-manager config set paths.work_dir  "$HOME/Movies/Familienfilm-Manager"

# 3. Erstes Video publizieren
familienfilm-manager publish "/pfad/zu/2026-04-30 Mein Familienvideo.mov"
```

Für Cloud-Quellen + Watch-Modus + KI-Auto-Vorschaubild siehe die jeweiligen Sektionen unten.

---

## Voraussetzungen

- Python 3.11+
- [ffmpeg](https://ffmpeg.org/) und `ffprobe` im `$PATH`
- [rclone](https://rclone.org/) im `$PATH` (für Deployments und Archivierung)
- [hdiutil](https://ss64.com/mac/hdiutil.html) (macOS, für ISO-Erstellung)
- [kurmann-mediaset-creator](https://pypi.org/project/kurmann-mediaset-creator/) (wird als Dependency installiert)

---

## Installation

```bash
uv pip install kurmann-familienfilm-manager
```

Im Entwicklungsmodus:

```bash
uv sync
```

---

## Verwendung (CLI)

### Gesamtworkflow

```bash
familienfilm-manager publish /pfad/zu/video.m4v \
  --title "Wanderung auf den Napf" \
  --category "Familie Kurmann-Glück" \
  --date "2025-03-24" \
  -v
```

Das Tool:
1. Extrahiert Metadaten (QuickTime-Tags oder Dateiname)
2. Erstellt ein Medienset via `mediaset-creator` mit separaten Profilen (Infuse/Web)
3. Deployt zu Infuse sofort bei Fertigstellung (direkt via rclone, kein ZIP-Entpacken)
4. Deployt zum Webserver nach Fertigstellung der Web-Komprimierung (rclone)
5. Archiviert die Originaldatei direkt (rclone copyto) + ``.settings.toml`` +
   neue ``.archive.toml`` mit konservierten Metadaten (Originalname, Export-mtime,
   Titel, Beschreibung) — kein TAR mehr seit v0.9.0, direkter Zugriff am NAS

### Einzeloperationen

```bash
# Nur Infuse-Deployment eines Verzeichnisses mit Infuse-Dateien
familienfilm-manager deploy-infuse /pfad/zum/infuse-verzeichnis/

# Nur Web-Deployment eines Web-ID-Verzeichnisses
familienfilm-manager deploy-web /pfad/zum/01JNXYZ.../

# Nur Archivierung
familienfilm-manager archive /pfad/zu/video.m4v --date 2025-03-24
```

### Optionen (publish)

| Option | Beschreibung |
|--------|-------------|
| `--title TEXT` | Video-Titel (überschreibt extrahierte Metadaten) |
| `--description TEXT` | Beschreibung des Videos |
| `--category TEXT` | Kategorie (z.B. «Familie Kurmann-Glück») |
| `--date TEXT` | Aufnahmedatum im ISO-Format (YYYY-MM-DD) |
| `--output-dir PATH` | Basisverzeichnis für die Web-Medienset-Ausgabe |
| `--poster-frame INT` | Frame-Nummer für Vorschaubild (überspringt KI-Auswahl) |
| `--poster-at FLOAT` | Zeitpunkt in Sekunden für Vorschaubild (z.B. 90 oder 1.5) |
| `--poster-crop TEXT` | Bildausschnitt (left/center-left/center/center-right/right) |
| `--web-id TEXT` | Web-ID für Web-Medienset (12 Zeichen, a-z 0-9; überschreibt Sidecar-Wert) |
| `--max-luminance INT` | Peak-Leuchtdichte in nits für HDR-Metadaten (Default: 1000) |
| `--no-sidecar` | Sidecar-Datei nicht lesen/schreiben |
| `--no-infuse` | Infuse-Deployment überspringen |
| `--no-web` | Web-Deployment überspringen |
| `--no-archive` | Archivierung überspringen |
| `--force` | Neuerstellung erzwingen |
| `--delete-source` | Originaldatei (+ `.settings.toml`) nach verifiziertem Publish löschen. Nur wenn Archiv aktiv und Size-verifiziert ist. Default: aus |
| `--web-budget` | Override Web-Speicherbudget für diesen Lauf (z.B. `'500GB'`, `'1TB'`). Überschreibt `web.storage_budget` aus Config |
| `--web-max-age` | Override max. Alter eines Web-Mediasets in Tagen (z.B. `365`). Überschreibt `web.max_age_days` aus Config |
| `--web-cleanup-dry-run` | Zeigt welche Web-Mediasets bei Budget-Überschuss oder Alter gelöscht würden, löscht aber nichts |
| `--verbose`, `-v` | Zusätzliche Ablaufinformationen auf stderr |

### Ausgabe

stdout enthält den Pfad zum erstellten Verzeichnis (Web-ID-Verzeichnis oder Infuse-Verzeichnis):

```
/pfad/zum/output/01JNXYZ.../
```

---

## Verzeichnis-Publikation (Batch)

Mit `publish-dir` werden alle qualifizierten Videos in einem Verzeichnis sequenziell publiziert
(älteste zuerst). Ideal für Watch-Folder-Workflows.

```bash
# Trockenlauf: zeige welche Videos publiziert würden
familienfilm-manager publish-dir /Movies/LumaFusion-Export/ --dry-run

# Alle qualifizierten Videos publizieren
familienfilm-manager publish-dir /Movies/LumaFusion-Export/

# Auch bereits publizierte Videos neu publizieren
familienfilm-manager publish-dir /Movies/LumaFusion-Export/ --force
```

### Filter-Kriterien

Nur Dateien werden publiziert, die **alle** Bedingungen erfüllen:

- Bekannte Video-Endung: `.mov`, `.mp4`, `.m4v`, `.mkv`
- Dateiname matcht das Muster `<YYYY-MM-DD> <titel>.<ext>` (mindestens Datum-Präfix)
- Kein Hidden-File (kein `.` am Anfang)
- Keine Verzeichnisse

Alles andere wird stillschweigend ignoriert.

### Idempotenz

Nach erfolgreicher Publikation wird ins Sidecar (`<video>.settings.toml`) geschrieben:

- Pro **aktivem Deploy-Kanal** ein `published_at`-Zeitstempel:
  `[web].published_at`, `[local].published_at`, `[archive].published_at`
- `source.size_bytes` – Dateigrösse der Quelle zu diesem Zeitpunkt
- `source.head_sha1` – SHA1 der ersten 1 MB der Quelldatei (Fingerprint)

Beim nächsten `publish-dir`-Lauf wird ein Video übersprungen, wenn **alle
aktiven Kanäle** einen `published_at`-Zeitstempel haben UND der
**Fingerprint der Quelldatei mit dem gespeicherten übereinstimmt** (gleiche Grösse
UND gleicher Hash der ersten 1 MB).

„Aktiv" heisst: ohne `--no-web`/`--no-infuse`/`--no-archive`-Flag. Wenn ein Kanal
deaktiviert ist, wird sein `published_at` für den Skip-Check nicht verlangt.

#### Warum Fingerprint statt Timestamps?

Die frühere Logik verglich `published_at` mit der `mtime` von Video und Sidecar.
Auf SMB-/NAS-Mounts (typisch für LumaFusion-Export auf ein NAS) ist das nicht
zuverlässig:

- **mtime-Auflösung schwankt** je nach Backend (teilweise 2-Sekunden-Raster,
  SMB-Metadaten-Caching)
- **Clock-Drift** zwischen lokaler Uhr (`datetime.now()`) und NAS-Filesystem
- **Drittsoftware verändert Timestamps** (Spotlight-Indexierung, Time Machine,
  Finder-Copy, Backup-Tools)

Resultat: Videos wurden zuverlässig neu publiziert, obwohl sie klar publiziert
waren. Der Fingerprint (Grösse + Kopf-Hash) ist unabhängig von Timestamps und
funktioniert identisch auf lokalem APFS und SMB.

Der Hash deckt nur die ersten 1 MB ab — ein pragmatischer Kompromiss aus
Scan-Geschwindigkeit (~100 ms auf SMB pro Video) und Kollisionssicherheit.
Ein Neu-Export ändert in aller Regel sofort Header-Bytes (Metadaten,
Container-Stream) und wird erkannt.

#### Republish-Trigger

- **Andere Dateigrösse** → Video wurde neu exportiert
- **Gleiche Grösse, anderer Kopf-Hash** → Header/Metadaten verändert
- **Altes Sidecar ohne Fingerprint** (aus Version ≤ 0.5.0) → einmaliger Republish
  erfasst den Fingerprint; ab dann idempotent

Mit `--force` werden alle Videos unabhängig vom Status neu publiziert.

**Hinweis:** Beim Einzeldatei-`publish` greift der Skip-Check **nicht** — der explizite
Aufruf publiziert immer.

### Optionen (publish-dir)

| Option | Beschreibung |
|--------|-------------|
| `--category TEXT` | Kategorie für alle Videos im Verzeichnis (überschreibt `defaults.category` aus Config) |
| `--no-infuse` | Infuse-Deployment für alle Videos überspringen |
| `--no-web` | Web-Deployment für alle Videos überspringen |
| `--no-archive` | Archivierung für alle Videos überspringen |
| `--force` | Auch bereits publizierte Videos neu publizieren |
| `--dry-run` | Nur anzeigen welche Videos publiziert würden, ohne zu publizieren |
| `--recursive`, `-r` | Unterverzeichnisse mit einbeziehen (siehe [Verzeichnis-Settings](#verzeichnis-settings-familienfilmtoml)) |
| `--work-dir PATH` | Arbeitsverzeichnis für temporäre Dateien (überschreibt `paths.work_dir` Config) |
| `--web-budget` | Override Web-Speicherbudget für diesen Batch (siehe [Web-Speicher-Budget](#web-speicher-budget)) |
| `--web-max-age` | Override max. Alter in Tagen (siehe [Web-Speicher-Budget](#web-speicher-budget)) |
| `--web-cleanup-dry-run` | Cleanup-Plan zeigen (Budget + Alter), nichts löschen |
| `--verbose`, `-v` | Zusätzliche Ablaufinformationen pro Video auf stderr |

### Verhalten bei Fehlern

Ein Fehler bei einem Video stoppt den Batch-Lauf nicht — die Verarbeitung läuft mit
dem nächsten Video weiter. Am Schluss wird eine Zusammenfassung mit Erfolgen,
übersprungenen und fehlgeschlagenen Videos ausgegeben. Exit-Code `1` wenn mindestens
ein Video fehlschlug.

### Robustes Deployment

Die drei rclone-basierten Deployment-Schritte (Infuse, Web, Archive) sind gegen
typische SMB/NAS-Flakeyness abgesichert:

- **Automatischer Retry** mit exponentiellem Backoff (1 s → 3 s → 9 s, 3 Versuche nach
  dem ersten). Deckt flüchtige Netzwerk-Hiccups und Filesystem-Sync-Delays ab, ohne
  den gesamten Publish-Workflow (oft 20+ min ffmpeg-Arbeit) wiederholen zu müssen.
  Jeder Retry wird im Terminal gelb angezeigt.
- **Post-Deploy Health-Check**: Nach erfolgreichem rclone-Lauf wird via `rclone lsf`
  geprüft, ob die erwarteten Dateien/Verzeichnisse wirklich im Ziel angekommen sind.
  Bei Mismatch: rot markierter Fehler statt silencieren.
- **Infuse-Medienset bleibt bei Deploy-Fehler erhalten** (Temp-Verzeichnis wird nicht
  aufgeräumt). Nachholen ohne Neuerstellung:
  ```bash
  familienfilm-manager deploy-infuse /pfad/zum/temp-verzeichnis/
  ```
  Der genaue Pfad wird im roten Fehler-Hinweis mitgeliefert.
- **Bei Archive-Deploy-Fehler bleibt die Originalquelle unangetastet** (v0.9.0
  nutzt `rclone copyto` statt `move`, und `--delete-source` löst erst nach
  Size-verifiziertem Upload aus). Einfach erneut ausführen.

---

## Verzeichnis-Überwachung (`watch-dir`)

Überwacht ein oder mehrere Verzeichnisse und publiziert neue Videos automatisch.
Ein Tick ist verhaltensidentisch zu einem `publish-dir`-Lauf — gleiche Idempotenz,
gleiche Filter-Regeln, gleiche Fehler-Isolation.

```bash
# Ein einziges Verzeichnis, Default-Intervall (24h):
familienfilm-manager watch-dir -d /Volumes/Videoschnitt/LumaFusion-Export

# Mehrere Verzeichnisse, alle 90 Minuten:
familienfilm-manager watch-dir -d /pfad/a -d /pfad/b --interval 90m

# Konfiguration in der Config hinterlegt, einfach starten:
familienfilm-manager config set watch.directories "/pfad/a,/pfad/b"
familienfilm-manager config set watch.interval_seconds "21600"   # 6h
familienfilm-manager watch-dir

# Einmaliger Durchlauf für launchd/cron:
familienfilm-manager watch-dir --once
```

#### Verhaltens-Defaults persistent setzen (v1.2.0+)

Wer regelmässig dieselben Optionen mitgibt, kann sie einmalig persistieren:

```bash
familienfilm-manager config set watch.recursive true
familienfilm-manager config set watch.delete_source true
```

Damit reicht später ein blosses `familienfilm-manager watch-dir` (oder
`watch-dir --once`). Pro Lauf ist das Verhalten via Bool-Pair-Flags
umkehrbar — `--no-recursive`, `--no-delete-source`, `--infuse`, `--archive`.

Vor jedem Lauf erscheint kurz auf stderr ein Konfigurations-Block, der
zeigt, welche Werte effektiv gelten und woher sie kommen (CLI / Config /
Default) — beim Debugging entfallen Vermutungen.

### Modi

| Modus | Beschreibung | Typischer Einsatz |
|-------|--------------|-------------------|
| **Daemon** (Default) | Endlosschleife: Tick → `sleep(interval)` → Tick → … Der erste Tick läuft sofort beim Start. | Manuell im Terminal, oder launchd mit `KeepAlive=true`. |
| **Single-shot** (`--once`) | Genau ein Tick, dann Exit 0. | launchd mit `StartInterval` / `StartCalendarInterval`, oder cron. |

Beide Modi führen einen Tick identisch aus — `--once` beendet danach nur die
Schleife. Umstieg vom Daemon auf launchd+`--once` ändert das Verhalten eines
einzelnen Laufs nicht.

### Intervall-Notation

`--interval` akzeptiert nackte Sekunden oder Suffixe `s` / `m` / `h` / `d`:

```
--interval 600      # 600 Sekunden = 10 Minuten
--interval 90m      # 90 Minuten
--interval 24h      # 24 Stunden (Default)
--interval 2d       # 2 Tage
```

Default ist **24 Stunden** — Familienfilme müssen nicht sofort publiziert werden,
ein täglicher Durchlauf schont das NAS (der Scan liest pro Video 1 MB zum
Fingerprint-Abgleich).

Im `--once`-Modus wird `--interval` ignoriert (mit Hinweis) — das Timing gehört
dem externen Scheduler.

### Stabilitätscheck für wachsende Dateien

LumaFusion-Exporte direkt auf SMB schreiben die Datei inkrementell. Würde der
Watch-Tick genau dann zuschlagen, wenn der Export noch läuft, würde eine
unvollständige Datei publiziert.

**Mechanismus**: Zu Beginn der Publish-Phase wird die Grösse aller Kandidaten
gemessen, anschliessend `--stability-window` Sekunden gewartet und erneut
gemessen. Dateien, deren Grösse sich geändert hat, werden als **„wächst noch"**
markiert und beim nächsten Tick erneut geprüft. Die Wartezeit gilt **einmalig
für alle Kandidaten gemeinsam** — bei 5 neuen Videos kostet das 60 s total,
nicht 5×60 s.

Default: `60s`. Mit `--stability-window 0` abschaltbar (oder via Config
`watch.stability_window_seconds`).

Wie beim Fingerprint-Check wird absichtlich die Dateigrösse geprüft, nicht die
mtime — aus denselben Gründen (SMB-Unzuverlässigkeit).

### Markdown-Benachrichtigung mit Live-Status

Ist `notification.file` konfiguriert, wird bei Publikationsbeginn ein
„In Bearbeitung"-Eintrag in eine Markdown-Datei geschrieben und beim
Abschluss durch den finalen Zustand ersetzt:

- **In Bearbeitung** — während der Publikation
- **Veröffentlicht** — mit Web-URL und/oder lokalem Zielpfad
- **Fehlgeschlagen — {Fehlermeldung}**
- **Abgebrochen** (wird beim Watch-Start gesetzt, falls vom letzten
  Lauf noch pending-Einträge liegenblieben, z.B. nach einem Crash)
- **Aus Web-Speicher entfernt am {Datum} ({Grund})** (nach Cleanup)

Jeder Eintrag hat eine stabile `video-id` (aus dem absoluten Pfad
abgeleitet) — ein Republish überschreibt denselben Eintrag statt
Duplikate anzuhängen. Die Datei begrenzt sich automatisch auf die
50 jüngsten Einträge (älteste fallen raus); damit bleibt sie auf
dem Telefon kurz und scrollbar.

Format der Datei:

```markdown
# Familienfilme

> _Diese Datei wird automatisch vom Familienfilm-Manager geschrieben._
> _Bitte nicht manuell editieren — Änderungen werden beim nächsten
> Publish überschrieben._

_Aktualisiert: 01.05.2026, 14:58_

## 01.05.2026 — Langnau im Emmental
> id: d969107420757722 · status: published · aktualisiert: 2026-05-01T12:57:45Z

- **Status:** Veröffentlicht
- **Aufnahmedatum:** 04.02.2024
- **Kategorie:** Familie
- **Web:** <https://mediathek.kurmann.swiss/mteyn0n1pm9d/>
- **Lokal:** `lyssach-nas:/Familienfilme/Final/2024`
```

Der Hinweis-Block-Quote oben warnt vor manueller Bearbeitung; die
Metadaten-Block-Quote pro Eintrag (`id`, `status`, `aktualisiert`)
dient FFM intern als State für Read-Modify-Write und Multi-Machine-
Merge — sie ist im Renderer als kleiner grauer Kasten sichtbar, was
ehrlich vermittelt, dass die Datei gemanagt ist.

#### Cloud-Sync via rclone

`notification.file` kann ein **lokaler Pfad** ODER ein **rclone-URI** sein:

```bash
# Lokal (default):
familienfilm-manager config set notification.file "/Users/me/Movies/Familienfilme.md"

# Cloud (z.B. mega.nz, hostpoint, Google Drive — alles was rclone kann):
familienfilm-manager config set notification.file "mega.nz:Familienfilme/Familienfilme.md"
```

Bei rclone-URIs hält FFM einen lokalen Cache unter
`~/.cache/familienfilm-manager/` und synchronisiert nach Real-Time-relevanten
Updates (`pending` / `published` / `failed` / `cleaned-up`). Auf
mega.nz und anderen Cloud-Plattformen wird die MD-Datei nativ
gerendert — Links sind direkt klickbar, ohne dass die Datei runter-
geladen werden muss.

**Konflikt-Strategie**: FFM gewinnt immer. Wer die Remote-MD manuell
editiert, während FFM läuft, wird beim nächsten Sync überschrieben.

#### Multi-Machine-Setups

Mehrere Rechner können dieselbe `notification.file` auf einem
gemeinsamen rclone-Target nutzen (z.B. zwei Macs mit eigenen Watch-
Quellordnern, aber einem geteilten `mega.nz:Familienfilme.md` für
die Status-Übersicht auf dem Telefon). Vor jedem Push wird der
aktuelle Remote-Stand gepullt und mit dem lokalen Inhalt vereinigt
(Union per `video-id`; bei Konflikt gewinnt der jüngere
`updated`-Timestamp). Damit überschreiben sich die Rechner nicht
gegenseitig.

Empfohlenes Setup: pro Rechner eigene Quellverzeichnisse, gemeinsames
Notification-Target. Geteilte Quellverzeichnisse (gleicher Watch-
Ordner auf zwei Rechnern) werden NICHT explizit unterstützt — dort
besteht das Risiko von Doppel-Publikationen.

**Sync-Fail-Toleranz**: bei Push-Fehler (Network, Auth) bleibt der Cache
aktuell — der nächste erfolgreiche Sync räumt auf. Eine Warnung erscheint
auf stderr, der Publish läuft weiter.

Interne Hygiene-Updates (z.B. die `aborted`-Markierung beim Watch-Start)
syncen nicht sofort — sie gehen beim nächsten echten Status-Update mit
raus. Hält die rclone-Aufrufe pro Watch-Tick gering.

### Fehler-Isolation

Fehler in einem Verzeichnis (fehlend, nicht lesbar, Exception) stoppen den
Watch-Lauf nicht. Andere Verzeichnisse werden weiter verarbeitet, beim
nächsten Tick wird erneut versucht. Fehler auf Video-Ebene (einzelne
Publikation schlägt fehl) werden wie bei `publish-dir` pro Video geloggt
und der Tick macht mit dem nächsten Video weiter.

### Beenden

SIGTERM und SIGINT (Ctrl+C) beenden den Daemon **sauber nach dem laufenden
Tick** — nicht mittendrin. Kompatibel mit `launchctl stop` und Docker-ähnlichen
Graceful-Shutdown-Mechanismen.

### launchd-Beispiel (macOS)

**Variante A — Daemon mit `KeepAlive`:**

```xml
<plist>
  <dict>
    <key>Label</key><string>ch.kurmann.familienfilm-manager</string>
    <key>Program</key><string>/usr/local/bin/familienfilm-manager</string>
    <key>ProgramArguments</key>
    <array>
      <string>familienfilm-manager</string>
      <string>watch-dir</string>
    </array>
    <key>KeepAlive</key><true/>
    <key>StandardErrorPath</key><string>/tmp/familienfilm-manager.err</string>
  </dict>
</plist>
```

**Variante B — `--once` mit Kalender-Trigger (täglich 03:00 Uhr):**

```xml
<key>ProgramArguments</key>
<array>
  <string>familienfilm-manager</string>
  <string>watch-dir</string>
  <string>--once</string>
</array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>3</integer></dict>
```

### Optionen (`watch-dir`)

| Option | Beschreibung |
|--------|-------------|
| `-d / --directory PATH` | Zu überwachendes Verzeichnis (mehrfach angebbar). Überschreibt `watch.directories`. |
| `--interval TEXT` | Intervall zwischen Ticks (`24h`, `90m`, `600`). Default: Config (24h). Im `--once`-Modus ignoriert. |
| `--once` | Genau ein Tick, dann Exit 0. |
| `--stability-window TEXT` | Wartezeit zur Erkennung wachsender Dateien. Default: Config (60s). `0` deaktiviert. |
| `--category TEXT` | Kategorie für alle Videos in allen überwachten Verzeichnissen (überschreibt `defaults.category` aus Config). |
| `--recursive`, `-r` | Unterverzeichnisse mit einbeziehen (siehe [Verzeichnis-Settings](#verzeichnis-settings-familienfilmtoml)) |
| `--no-infuse` / `--no-web` / `--no-archive` | Profile überspringen (wie bei publish-dir). |
| `--force` | Auch bereits publizierte Videos neu publizieren. |
| `--delete-source` | Pro Video: Originaldatei (+ `.settings.toml`) im überwachten Verzeichnis löschen, sobald alle aktiven Kanäle erfolgreich und das Archiv Size-verifiziert ist. Ideal für unattended Watch auf Export-Verzeichnissen (Platz wird automatisch frei). |
| `--web-budget` | Override Web-Speicherbudget für diesen Watch-Lauf (siehe [Web-Speicher-Budget](#web-speicher-budget)) |
| `--web-cleanup-dry-run` | Cleanup-Plan zeigen, nichts löschen |
| `--work-dir PATH` | Arbeitsverzeichnis (überschreibt `paths.work_dir`). |
| `--verbose`, `-v` | Zusätzliche Ablaufinformationen auf stderr. |

---

## Web-Speicher-Budget

Das Web-Target (typisch Hostpoint) hat in der Regel ein hartes Speicherlimit
(z.B. 500 GB). Damit unattended `watch-dir`-Läufe nicht in den Vollläufer
laufen, kann ab v0.10.0 ein Speicherbudget **und/oder** eine maximale
Aufbewahrungsdauer pro Mediaset konfiguriert werden. Vor jedem Web-Upload
laufen zwei Cleanup-Pässe in dieser Reihenfolge:

1. **Age-Pass** (`web.max_age_days`): alles löschen, was älter als die
   Schwelle ist — unabhängig vom Budget.
2. **Budget-Pass** (`web.storage_budget`): falls noch immer zu wenig Platz,
   ältester Rest weg, bis (used + new + 5 %-Marge) ins Budget passt.

Beide Constraints sind unabhängig: nur Budget, nur Age, beides oder keines.

### Konfiguration

```bash
# Speicherbudget (optional)
familienfilm-manager config set web.storage_budget 500GB

# Maximales Alter eines Web-Mediasets in Tagen (optional)
familienfilm-manager config set web.max_age_days 365
```

`storage_budget`-Suffixe: `B`, `K`/`KB`, `M`/`MB`, `G`/`GB`, `T`/`TB`
(case-insensitiv, SI-Basis = 10⁹). Leerer Wert → kein Cleanup.

`max_age_days` ist ein einfacher Integer in Tagen. Leerer Wert oder `0` →
keine Altersbegrenzung.

### Trigger-Verhalten (lazy)

Beide Cleanup-Pässe laufen **nur vor einem Web-Upload**. Es gibt keinen
separaten Hintergrund-Job. Wenn lange nichts publiziert wird, können
Mediasets im Web durchaus älter als die Schwelle bleiben — pragmatisch und
ohne Überraschungen.

### Wie der Budget-Pass rechnet

```
benötigt = aktuell_belegt + neues_mediaset
maximum = budget × 95 %    # 5 % Sicherheitsmarge
```

Wenn `benötigt > maximum`, wird das **älteste Mediaset** (gemessen an der
`mtime` der `index.html` im Mediaset-Verzeichnis) per `rclone purge` gelöscht.
Iteriert solange, bis Platz reicht.

### Konsole loggt explizit

```
Web-Alter-Check: max 365 Tage, 2 von 17 Mediaset(s) zu alt
Web-Cleanup (Alter): lösche q26qobjxl4c3 (220 MB, 412 Tage alt — älter als max 365)
Web-Budget: belegt 421.2 GB / 500 GB (Marge 5% = 25 GB), neuer Upload 220 MB
```

### Effekt auf `Familienfilme.md`

Bereinigte Einträge bekommen den Status `cleaned-up` und eine
sprechende Status-Zeile:

```markdown
- **Status:** Aus Web-Speicher entfernt am 27.04.2026 (älter als 365 Tage)
- **Lokal:** `nas:/Familienfilme/Final/2024`
```

Mögliche Gründe in Klammern: „älter als 365 Tage" oder
„Speicherplatz". Der Lokal-Pfad bleibt sichtbar — der Film bleibt
auf dem NAS, nur die Web-Kopie ist weg. Die Web-Zeile bleibt
ebenfalls als historische Spur erhalten; der Status-Text macht klar,
dass die URL nicht mehr live ist.

### Pro-Lauf-Overrides

| Flag | Wirkung |
|------|---------|
| `--web-budget '1TB'` | Übersteuert `web.storage_budget` für diesen Lauf |
| `--web-max-age 90` | Übersteuert `web.max_age_days` für diesen Lauf (Tage) |
| `--web-cleanup-dry-run` | Zeigt den Lösch-Plan beider Pässe, löscht aber nichts (Upload erfolgt trotzdem) |

### Hinweis zu Infuse + Archive

Beide Kanäle bleiben **unbeschränkt** — sie laufen gegen NAS-Storage,
das aus Sicht des Managers als grenzenlos gilt. Es gibt dort weder
Budget- noch Age-Cleanup.

### Status-Übersicht

```bash
familienfilm-manager web-storage status
```

Zeigt Read-only ohne Änderung:

```
Web-Target: hostpoint:
Budget: 500.00 GB (Marge 5% = 25.00 GB)
Max-Age: 365 Tage

Mediasets: 36
Belegt: 98.54 GB
  → 19.7% des Budgets
  → effektiv frei (mit Marge): 376.46 GB

Ältestes:  q26qobjxl4c3 (220.00 MB, 102 Tage alt)
Neuestes:  k2x7sifsp7ja (1.45 GB, 0 Tage alt)

→ Erstes Mediaset wird in 263 Tag(en) alters-rotiert (bei nächstem Web-Upload).
```

Override für „was-wäre-wenn"-Szenarien: `--web-budget 1TB`, `--web-max-age 90`.

---

## KI-Vorschaubild-Wahl & Persistierung

Wenn du beim Publish kein explizites Vorschaubild angibst (kein
`--poster-frame`/`--poster-at`, kein `poster-Xs`-Filename-Suffix, kein
Sidecar-Wert), wählt der **Vorschaubild-Manager via Claude** Frame +
Crop. Seit v0.11.0 wird die Wahl ins `.settings.toml` zurückgeschrieben:

```toml
[poster]
frame_number = 142
crop_position = "center"
selected_by = "ai"
ai_reasoning = """
Frame zeigt schönes Lächeln und gute Belichtung.
Crop center wegen zentraler Bildkomposition.
"""
```

**Beim nächsten Republish** liest der Familienfilm-Manager diese Werte
aus dem Sidecar und übergibt sie als „manuelle" Vorgabe an den
Mediaset-Creator — **kein erneuter KI-Aufruf, kein Token-Verbrauch**,
gleicher Frame, gleicher Crop. Stabilität + Kosten-Effizienz.

### Hierarchie der Poster-Wahl

Bei jedem Publish wird in dieser Reihenfolge der erste gefundene Wert
genommen:

1. **CLI-Argumente** (`--poster-frame`, `--poster-at`, `--poster-crop`)
2. **Filename-Suffix** (`Video poster-2m14s700.mov`)
3. **Sidecar `[poster]`** (egal ob `selected_by="manual"` oder `"ai"`)
4. **Nichts da** → KI im Vorschaubild-Manager wählt + Wahl wird persistiert

Ein **explizites User-Argument überschreibt** auch eine bestehende KI-Wahl
im Sidecar — wenn du also unzufrieden mit der KI-Wahl bist, einfach
`--poster-at 47.5` setzen und die alte Wahl wird beim Re-Publish ersetzt.

### `selected_by`-Werte

| Wert | Bedeutung |
|------|-----------|
| `"cli"` | User hat per CLI-Argument explizit gewählt (`--poster-frame`/`--poster-at`/`--poster-crop`) |
| `"filename"` | Aus dem Datei­namen-Suffix `poster-Xs` abgeleitet |
| `"sidecar"` | Aus bestehender Sidecar gelesen (Ursprung in dieser Sidecar-Datei nicht weiter dokumentiert — z.B. pre-v0.11.0 oder per Hand editiert) |
| `"ai"` | Claude im Vorschaubild-Manager hat gewählt; `ai_reasoning` enthält die Begründung |
| `"sidecar-image"` | Neben dem Video lag ein Sidecar-Bild (z.B. `.jpg`), das direkt als Vorlage genommen wurde — kein Frame-Index |

**KI-Origin-Schutz**: Wenn ein Sidecar-Eintrag ursprünglich von der KI stammt (`selected_by = "ai"`) und beim nächsten Republish nichts Neues vom User kommt, wird `selected_by = "ai"` **beibehalten** — du verlierst die Information „KI-Wahl" nicht, nur weil sie zwischenzeitlich ins Sidecar gewandert ist.

---

## Poison-Video-Detection

Im `watch-dir`-Modus läuft der Publisher unattended. Wenn ein Video
wiederholt scheitert (z.B. wegen falschem `poster-Xs`-Suffix oder
defektem Codec), wäre es früher bei jedem Tick erneut versucht
worden — endlos, alle 5 Minuten.

Seit v0.11.0:
- Bei jedem Fehlversuch zählt das Sidecar `[publish].failure_count` hoch.
- Ab **3 aufeinander folgenden Fehlversuchen** UND **unveränderter
  Source-Datei** wird das Video als `POISONED` geskippt — mit klarer
  Meldung in der Konsole:
  ```
  Poison-Skip: Video.mov (3 Fehlversuche, Source unverändert).
  Mit --force erzwingen.
  ```
- Sobald die Source-Datei geändert wird (anderer Fingerprint), startet
  der Counter wieder bei 0 — neue Datei verdient frischen Versuch.
- Ein erfolgreicher Publish löscht den Counter ebenfalls.
- `--force` umgeht die Poison-Sperre (für gezieltes Re-Try ohne
  Source-Änderung).

Damit terminieren `watch-dir`-Loops auf bekannt kaputten Videos
zuverlässig.

---

## rclone-Quellen

Statt SMB/NAS-Mounts kannst du seit v0.7.0 **rclone-Pfade direkt als Quelle**
angeben (`remote:path`). Alle drei Befehle (`publish`, `publish-dir`,
`watch-dir`) akzeptieren rclone-URIs. Lokale Pfade und rclone-URIs können in
`watch.directories` frei gemischt werden (komma-separiert).

```bash
familienfilm-manager config set paths.work_dir "/Users/du/Movies/Familienfilme"
familienfilm-manager config set watch.directories "lyssach-nas:/Familienfilme/Quelle"
familienfilm-manager watch-dir --once -v
```

### Warum rclone statt SMB-Mount?

> ⚠️ **Wichtig — Bekannte Inkompatibilität**: Auf macOS kann ein **SMB-Mount
> zum NAS nicht gleichzeitig mit rclone-SFTP-Verbindungen zum selben NAS**
> genutzt werden. Der aktive SMB-Mount belegt die Netzwerkroute exklusiv;
> parallele SFTP-Versuche scheitern mit `no route to host` (ARP-/Routing-
> Konflikt). Diagnostiziert in v0.6.0 nach langem Debugging.
>
> **Konsequenz**: Wenn dein NAS **gleichzeitig** als Quelle und als
> Deploy-Ziel dient (Infuse/Archive), **darfst du es nicht als SMB-Share
> einbinden**. Nutze stattdessen einen rclone-Remote für beide
> Richtungen.
>
> Wenn Quelle und Ziel auf **unterschiedlichen NAS** liegen, kann der
> SMB-Mount der Quelle funktionieren — aber rclone-Quellen sind trotzdem
> empfohlen (Staging-Verzeichnis + Light-Peek profitieren).

Mit rclone in beide Richtungen — lesen vom NAS, schreiben zum NAS (Infuse,
Archive) — gibt es keinen SMB-Mount mehr, das Problem ist vermieden.
Ein eventuell noch vorhandener SMB-Mount zum selben NAS sollte in
Finder → Gehe zu → Mit Server verbinden getrennt werden (oder via
`diskutil unmount /Volumes/<ShareName>`).

### Wie läuft ein Publish mit rclone-Quelle ab?

1. **Staging** — Video plus alle stem-identischen Sidecars
   (`.settings.toml`, `.fcpxml`, `.lfpackage`, Fanart etc.) werden via
   `rclone copyto` ins `paths.work_dir/stage-<uuid>/` kopiert. Das Staging-
   Verzeichnis ist pro Publish isoliert.
2. **Lokaler Publish** — ab hier läuft der Publisher unverändert: ffprobe,
   mediaset-Erstellung, Infuse- und Web-Deploy, Archivierung. Alles
   passiert auf der lokalen Kopie — kein Netzwerk-Stress.
3. **Sidecar-Rücksync** — nach erfolgreichem Publish wird die aktualisierte
   `.settings.toml` via `rclone copyto` **zurück an die Original-Position**
   auf dem Remote geschrieben. Idempotenz (per-Kanal `published_at` +
   Fingerprint) bleibt voll erhalten.
4. **Cleanup** — das Staging-Verzeichnis wird entfernt.

### Light-Peek Skip-Check

Bei `publish-dir` / `watch-dir` gegen eine rclone-Quelle wird für die
Idempotenz-Prüfung **nur das kleine Sidecar** vom Remote geladen, nicht das
Video. Wenn alle aktiven Kanäle ein `published_at` haben und die im Sidecar
gespeicherte `source.size_bytes` mit der aktuellen Remote-Size übereinstimmt,
wird das Video übersprungen — ohne einen einzigen Byte Video-Traffic. Beim
Daemon-Lauf mit 100 Videos und 3 neuen fallen damit nur die 3 neuen ins
Gewicht.

**Limitation**: Head-SHA1-Fingerprint entfällt bei rclone-Quellen (würde
volles Staging erfordern). Size-Match allein ist die Fingerprint-Basis.
Bei Neu-Export ändert sich die Size in aller Regel ebenfalls — zuverlässig
genug für den praktischen Fall.

### Voraussetzungen

- **rclone muss konfiguriert sein** für das Remote — genauso wie für die
  Deploy-Ziele. Der `mediaset-creator` braucht kein Extra-Setup.
- **`paths.work_dir` muss gesetzt sein** für rclone-Quellen (Staging-Ziel).
  Ohne diese Config gibt es einen deutlichen Fehler vor dem Staging:
  ```
  rclone-Quelle nas:/Quelle braucht ein work_dir zum Stagen.
  Setze paths.work_dir in der Config.
  ```
- **familienfilm.toml** kann auch auf dem Remote liegen. Die Settings-Kette
  läuft via `rclone cat` nach oben bis Remote-Root (`remote:`).

### Nicht in v0.7.0

- **Stabilitätscheck** (gegen wachsende Dateien während eines LumaFusion-
  Exports) ist bei rclone-Quellen aktuell inaktiv. Lokale Quellen nutzen
  ihn weiter.
- **Mixed-Kontext**: lokale Video-Quelle mit `familienfilm.toml` auf
  Remote (oder umgekehrt) wird nicht unterstützt — die Settings-Suche
  folgt strikt dem Quelltyp der Video-URI.

---

## Metadaten-Extraktion

Metadaten werden in folgender Priorität ermittelt:

<a name="aufnahmedatum-hierarchie-v130"></a>
### Aufnahmedatum-Hierarchie (v1.3.0+)

**Grundprinzip:** der Manager arbeitet auf Schnitt-Exporten. Container-Datums-Tags
(`creation_time`, `com.apple.quicktime.creationdate`) sind bei diesen Exporten
strukturell unbrauchbar als Aufnahmedatum – sie zeigen den Render-Zeitpunkt,
nicht den Aufnahme-Zeitpunkt der Original-Clips. Sie werden daher **nicht
ausgelesen**.

Stattdessen gilt diese Reihenfolge (höchste Priorität zuerst):

| # | Quelle | Beispiel | Verfügbar |
|---|---|---|---|
| 1 | **CLI `--date`** | `--date 2024-08-14` | immer |
| 2 | **Dateiname** | `2024-08-14 Familie.mov` → `2024-08-14` | wenn `YYYY-MM-DD`-Präfix gesetzt |
| 3 | **Sidecar-Cache** | `[metadata].recording_date` in der `.settings.toml` | nach erstem Schnittprojekt-Lookup automatisch |
| 4 | **Schnittprojekt-Lookup** | aus FCPXML/iMovieMobile via [`kurmann-schnittprojekt-leser`](https://github.com/kurmann/schnittprojekt-leser) | wenn `schnittprojekt.directories` konfiguriert |
| 5 | (kein Datum) | – | hartes Scheitern oder „ignoriert" – siehe unten |

**Wenn keine der vier Quellen ein Datum liefert:**

- **Mit `schnittprojekt.directories`-Config**: Status `ignoriert` (Soft-Skip). Das
  Schnittprojekt kann später noch im Lookup-Verzeichnis erscheinen – Patrick's
  Workflow erlaubt z. B., dass der MOV-Export schon fertig ist, das Schnittprojekt
  aber erst Stunden später exportiert wird. Der Watch-Loop versucht es beim
  nächsten Tick automatisch wieder. **Kein Failure-Counter, kein Poison.**
- **Ohne Config**: Hartes Scheitern mit klarer Fehlermeldung. Der User hat dann
  Filename-Datum-Disziplin als einzigen Kanal und muss entweder umbenennen oder
  `--date` setzen.

**Container-Tags werden weiterhin gelesen für:**

- **Titel** (`com.apple.quicktime.title` → fällt auf Filename zurück)
- **Beschreibung** (`com.apple.quicktime.description` → meist von FCP/LumaFusion gesetzt)

…aber **nie für das Aufnahmedatum**. Ihre Aussagekraft als Aufnahmezeit ist
strukturell zerstört, sobald der Schnitt-Export läuft.

#### Dateiname-Suffixe, die ignoriert werden

Damit der Match (für Lookup) und die Titel-Extraktion (für Archiv-Filename)
sauber funktionieren, werden zwei Suffix-Familien automatisch gestrippt:

- **Poster-Suffix** (` poster-2min`, ` poster-30s500`, …) – steuert nur den
  Vorschau-Frame und gehört nicht in den Titel
- **iOS-iMovie-Export-Suffix** (`-4K60H`, `-4K60`, `-4K30`, `-4K24`, `-UHD`,
  `-1080p60`, `-1080p`, `-720p`, `-540p`, `-360p`) – Lieferformat-Marker
  von iMovie iOS, kein Titel-Teil

Beide Reihenfolgen werden gehandhabt:

| Filename | → Title-Basename |
|---|---|
| `Familie poster-2min.mov` | `Familie` |
| `Familie-4K60H.mov` | `Familie` |
| `Familie poster-2min-4K60H.mov` (Schnitt mit Poster, dann iMovie-Export) | `Familie` |
| `Familie-4K60H poster-2min.mov` (manuell umbenannt) | `Familie` |
| `2024-08-14 Familie poster-30s-4K60H.mov` | `Familie`, Datum `2024-08-14`, Poster bei 30s |
| `Hochzeit-2024.mov` (Jahres-Suffix, kein Codec-Marker) | `Hochzeit-2024` (unangetastet) |
| `Episode-3.mov` (Episodennummer) | `Episode-3` (unangetastet) |

#### Schnittprojekt-Lookup (v1.3.0+)

Wenn weder Filename noch QuickTime-Tags noch generische Tags ein Datum liefern, kann der Familienfilm-Manager das Aufnahmedatum direkt aus dem zugrundeliegenden Schnittprojekt extrahieren – via [`kurmann-schnittprojekt-leser`](https://github.com/kurmann/schnittprojekt-leser) (FCPXML aus Final Cut Pro / LumaFusion sowie iMovieMobile-ZIP aus iOS-iMovie).

**Greift in allen drei Publish-Befehlen** (`publish`, `publish-dir`, `watch-dir`), weil sie alle über dieselbe `run_publish()`-Komposition gehen.

**Konfiguration** – ein oder mehrere Verzeichnisse, lokal **oder** rclone-URI, komma-separiert:

```bash
familienfilm-manager config set schnittprojekt.directories \
  "lyssach-nas:/Videoschnitt/iMovie-Export, lyssach-nas:/Videoschnitt/Luma Fusion-Export"

# Ein lokales Volumen + ein rclone-Remote
familienfilm-manager config set schnittprojekt.directories \
  "/Volumes/Schnitt, lyssach-nas:/Videoschnitt/Luma Fusion-Export"
```

**Konsolenausgabe beim Treffer** (immer sichtbar, auch ohne `-v`):

```
[recording_date_from_schnittprojekt] Aufnahmedatum aus Schnittprojekt:
  2026-04-23 / 2026-04-23T17:52:21+02:00
  (Quelle: lyssach-nas:/Videoschnitt/iMovie-Export/2026-04-23 IKEA Hotdog-Spielecke mit Katie.iMovieMobile,
   imovie:editList:creationDate, high)
```

**Bei Cache-Hit aus früherem Lookup:**

```
[recording_date_from_sidecar_cache] Aufnahmedatum aus Sidecar-Cache:
  2026-04-23 (Quelle: lyssach-nas:/.../IKEA.iMovieMobile)
```

**Mit `-v`** kommen zusätzlich `[schnittprojekt_lookup_started]` (zu Beginn) und `[schnittprojekt_lookup_not_found]` (kein Treffer) auf stderr.

**Match-Logik:** vom Video-Stem werden in dieser Reihenfolge entfernt:

1. **Poster-Suffix** (` poster-2min`, ` poster-30s` etc.)
2. **iOS-iMovie-Export-Suffix** (`-4K60H`, `-4K60`, `-4K30`, `-UHD`, `-1080p60`, `-1080p`, `-720p`, `-540p`, `-360p` – die typischen iMovie-iOS-Codec-/Auflösungs-Marker)
3. **Datum-Präfix** (`YYYY-MM-DD `)

Der so gewonnene „Title-Basename" wird NFC-normalisiert und case-insensitiv mit dem Schnittprojekt-Stem verglichen (Schnittprojekt darf eigenes Datum-Präfix tragen, das wird ebenfalls entfernt):

| Video-Datei | Title-Basename | Match auf Schnittprojekt |
|---|---|---|
| `Familie Sommer.mov` | `familie sommer` | `Familie Sommer.iMovieMobile` ✅ |
| `Familie Sommer poster-2min.mov` | `familie sommer` | `2024-08-14 Familie Sommer.fcpxmld` ✅ |
| `Kathrin an der Emme im Frühling-4K60H.mov` | `kathrin an der emme im frühling` | `Kathrin an der Emme im Frühling.iMovieMobile` ✅ |
| `Familie-1080p60.mov` | `familie` | `Familie.fcpxmld` ✅ |
| `Käthrin.mov` (NFD) | `käthrin` (NFC) | `Käthrin.fcpxml` (NFC) ✅ |
| `Andere Familie.mov` | `andere familie` | `Familie Sommer.fcpxml` ❌ |
| `Hochzeit-2024.mov` | `hochzeit-2024` | – | (Jahres-Suffix wird **nicht** als Codec-Marker erkannt – nur explizite iMovie-Patterns matchen) |

Bei rclone-URIs lädt das Tool das gefundene Schnittprojekt vor dem Lesen ins Temp-Verzeichnis (`rclone copyto` für Single-File, `rclone copy` für Bundle-Verzeichnis) und räumt nachher auf.

**Idempotenz:** das gefundene Datum wird in die `.settings.toml` geschrieben:

```toml
[metadata]
recording_date = "2026-04-23"
recording_datetime = "2026-04-23T17:52:21+02:00"
recording_date_source = "lyssach-nas:/Videoschnitt/Familie Sommer.iMovieMobile"
```

`recording_date` ist immer YYYY-MM-DD (Filename-tauglich – das Pattern `{recording_date}-{slug}.{ext}` im Archiver bleibt unverändert). `recording_datetime` enthält den vollen Wallclock-Zeitstempel mit TZ-Offset (z. B. für künftige Text-Overlays im Schnitt oder Notifier-Anzeigen mit Uhrzeit). Beim nächsten Publish desselben Videos überspringt der Manager den teuren Lookup und nimmt die Cache-Werte. Wenn du den Cache invalidieren willst (z. B. weil das Schnittprojekt umbenannt wurde), lösche die Felder aus der `.settings.toml`.

**Lookup wird nicht ausgeführt**:

- wenn `schnittprojekt.directories` nicht konfiguriert ist
- wenn `recording_date` bereits in der `.settings.toml` steht
- wenn das Datum aus dem Filename-Präfix oder CLI `--date` ermittelt wurde

**Wenn der Lookup fehlschlägt** (kein passendes Schnittprojekt im konfigurierten
Verzeichnis, weder via Filename noch Sidecar ein Datum), wird das Video als
**ignoriert** klassifiziert (siehe Hierarchie oben), nicht als fehlgeschlagen.
Das ist die Realisierung des „warten auf Schnittprojekt"-Workflows.

**Konfidenz-Schwelle (v1.3.0+)**: der Manager akzeptiert Lookup-Ergebnisse nur
mit Konfidenz `high` oder `medium`. Liefert der Leser nur `low` (typisch
`project_fallback` = mtime der Projektdatei oder `file_mtime` der Quelldatei),
wird das Video ebenfalls als **ignoriert** behandelt – das wäre kein
verlässliches Aufnahmedatum, sondern bloss der Render-/Schreib-Zeitpunkt.

**Suffix-Präferenz** der Schnittprojekt-Endungen (höchste zuerst):

1. `.fcpxmld` (Bundle-Verzeichnis, beste Konfidenz)
2. `.iMovieMobile` (ZIP, Datum direkt im Plist)
3. `.fcpxml` (Single-File, abhängig von externen Quellen)

##### Warum Bundle vor Single-File? – FCPXML-Hintergrund

Final Cut Pro und LumaFusion exportieren FCPXML in zwei Varianten, die
**logisch gleich** sind, aber **physisch unterschiedlich**:

```
Projekte/
├── Familie.fcpxml                         ← Single-File-XML
│   (referenziert src="./A001_….mov", aber die Datei liegt
│    physisch NICHT hier daneben – sondern im FCP-Library oder
│    auf einem anderen Mac-Volume)
│
└── Familie.fcpxmld/                       ← Bundle-Verzeichnis
    ├── info.fcpxml                        (gleicher src-Pfad)
    ├── A001_….mov                         ← physisch IM Bundle
    ├── A001_….mov                         ← physisch IM Bundle
    └── …
```

Beide XML-Dateien tragen typischerweise dieselben `src`-Attribute (relative
Pfade wie `./A001_….mov` oder absolute Library-Pfade). Der `schnittprojekt-leser`
folgt diesen Pfaden **wörtlich relativ zur XML-Datei**, um die Quelldatei zu
finden und mit exiftool das Aufnahmedatum zu extrahieren.

| Variante | `src`-Auflösung | Konfidenz |
|---|---|---|
| **Bundle** (`info.fcpxml` im `.fcpxmld/`) | findet `A001_….mov` im selben Bundle | `high` (echtes EXIF-Datum) |
| **Single-File** (`.fcpxml` allein) | findet `A001_….mov` nicht (liegt nicht daneben) | `low` (nur `project_fallback` = mtime) |

Der Familienfilm-Manager wählt deshalb das Bundle, wenn beide existieren –
nur so kommt der Leser an die echten Aufnahmedaten. Der `schnittprojekt-leser`
selbst sucht **nicht** automatisch im benachbarten Bundle; er folgt strikt
den `src`-Pfaden, was ein bewusst klares, vorhersehbares Verhalten ist.

#### Konsolen-Sichtbarkeit der Lookup-Schichten

Bei aktiver `-v`-Option werden auch die internen Stages des
`schnittprojekt-leser`-Aufrufs als Events durchgereicht – mit klarem Prefix:

```
[schnittprojekt_lookup_started] Suche Schnittprojekt für '...' in 1 Verzeichnis(en) …
[schnittprojekt_leser:format_detection_started] [via kurmann-schnittprojekt-leser] Format-Detection für /tmp/.../...
[schnittprojekt_leser:format_detected] [via kurmann-schnittprojekt-leser] Format erkannt: fcpxml_bundle
[schnittprojekt_leser:fcpxml_parsing_started] [via kurmann-schnittprojekt-leser] Parse FCPXML info.fcpxml
[schnittprojekt_leser:fcpxml_parsed] [via kurmann-schnittprojekt-leser] FCPXML geparst: Schema 1.10, 5 Top-Level-Spine-Clips
[schnittprojekt_leser:source_file_resolved] [via kurmann-schnittprojekt-leser] Quelldatei: A001_05030726_C446.mov
[schnittprojekt_leser:exiftool_invoked] [via kurmann-schnittprojekt-leser] exiftool liest A001_05030726_C446.mov
[schnittprojekt_leser:metadata_extracted] [via kurmann-schnittprojekt-leser] exiftool-Output verarbeitet
[recording_date_from_schnittprojekt] Aufnahmedatum via kurmann-schnittprojekt-leser: 2026-05-03 / 2026-05-03T07:26:56+02:00 (Schnittprojekt: ..., Provenance: exif:Keys:CreationDate, Konfidenz: high)
```

Damit ist jederzeit transparent, welche Komponente das Datum tatsächlich
liest. Auch ohne `-v` sieht man immer noch das finale Result-Event mit
expliziter „via kurmann-schnittprojekt-leser"-Kennzeichnung.

#### Auswirkung auf publish-dir und watch-dir (v1.3.0+)

Der Filter „nur Files mit `<YYYY-MM-DD>`-Präfix sind qualifiziert" wird gelockert, sobald `schnittprojekt.directories` konfiguriert ist. Files **ohne** Datum-Präfix werden dann ebenfalls in den Scan aufgenommen – ihr Datum kommt aus dem Lookup oder dem Sidecar-Cache. Beispiel:

```
Schnitte/Internet/Kathrin an der Emme im Frühling-4K60H.mov   ← jetzt qualifiziert
Schnitte/Internet/2025-01-12 Hochzeit.mov                      ← weiter qualifiziert
```

Ist `schnittprojekt.directories` **nicht** gesetzt, gilt das alte Verhalten: Files ohne Datum-Präfix werden als „ignoriert" gemeldet, damit Tippfehler nicht still verschwinden.

### Titel

1. **CLI-Parameter** (`--title`) – überschreibt alles
2. **QuickTime-Tags** (via ffprobe) – aus Final Cut Pro exportierte Metadaten
3. **Dateiname** – Titel nach dem Datumspräfix:
   - `2025-03-24 Wanderung auf den Napf.m4v` → «Wanderung auf den Napf»
   - `2025-03-24 - Wanderung auf den Napf.m4v` → «Wanderung auf den Napf»

### Poster-Zeitpunkt aus dem Dateinamen

Der Zeitpunkt für das Vorschaubild kann direkt im Dateinamen mitgegeben werden – ideal für
Videoschnitt-Programme wie LumaFusion, die keine Sidecar-Dateien beim Export erstellen.

**Format:** ` poster-<zeit>` am Ende des Dateinamens (Leerzeichen davor).
Minuten-Marker: `min` (vollständig) oder `m` (Kurzform, ffmpeg-Stil).

| Suffix | Bedeutung |
|--------|-----------|
| `poster-2min` / `poster-2m` | 2 Minuten = 120 Sek. |
| `poster-3min12` / `poster-3m12` | 3 Min 12 Sek = 192 Sek. |
| `poster-3min11s` / `poster-3m11s` | 3 Min 11 Sek (mit trailing `s` als Sekunden-Marker) |
| `poster-1min30s500` / `poster-1m30s500` | 1 Min 30.5 Sek (Sub-Sekunden) |
| `poster-30s` | 30 Sek. |
| `poster-30s500` | 30.5 Sek. |

**Beispiel:**

```
2026-04-16 HDR-Test poster-1min30.mov
```

→ Datum: `2026-04-16`, Titel: «HDR-Test», Poster-Zeitpunkt: 90 Sek.

Der Suffix wird beim Parsen vom Titel entfernt (landet nicht im Output-Dateinamen) und
automatisch in die Sidecar-Datei (`.settings.toml`) geschrieben. Bei einem späteren
`publish` ohne Suffix im Dateinamen wird der Wert aus der Sidecar-Datei gelesen.

**Priorität:** CLI (`--poster-at`) > Filename-Suffix > Sidecar.

**Wichtig:** Ein Aufnahmedatum ist zwingend erforderlich. Ohne Datum wird die Verarbeitung abgebrochen.

---

## Settings-Sidecar-Datei

Wenn Poster-Einstellungen via CLI angegeben werden (`--poster-frame`, `--poster-at`, `--poster-crop`), speichert der Familienfilm Manager diese automatisch in einer `.settings.toml`-Datei neben dem Video:

```
2025-03-24 Wanderung auf den Napf.m4v
2025-03-24 Wanderung auf den Napf.settings.toml   ← automatisch erstellt
```

Inhalt:
```toml
[poster]
timestamp_seconds = 42.5
crop_position = "center-right"

[web]
web_id = "a3xk9mn2pqrw"
published_at = "2026-04-18T10:23:45"

[local]
published_at = "2026-04-18T10:24:12"

[archive]
published_at = "2026-04-18T10:26:58"

[source]
size_bytes = 4521830394
head_sha1 = "a3f4c9b2..."

[metadata]
category = "Familie Kurmann-Glück"
```

- **`[poster]`** — Poster-Einstellungen (bei erneutem Publish wiederverwendet)
- **`[web]`** — Web-ID, Luminance, Publish-Zeitpunkt der **Web-Publikation** (Hostpoint o.ä.)
- **`[local]`** — Publish-Zeitpunkt der **Infuse-/LAN-Medienserver-Publikation**. Heißt
  `[local]` (nicht `[infuse]`), weil der Kanal einen lokalen Medienserver beliefert —
  Infuse ist nur eine mögliche Consumer-App
- **`[archive]`** — Publish-Zeitpunkt der **Archivierung** (direkte Kopie des Originals im Langzeit-Archiv, seit v0.9.0 kein TAR mehr)
- **`[source]`** — Fingerprint für die `publish-dir`-Idempotenz (siehe [Idempotenz](#idempotenz))
- **`[metadata]`** — Effektive Metadaten zum Publish-Zeitpunkt. Macht das Archiv
  **selbsttragend**: wenn ein Video später aus dem Archiv extrahiert und mit
  `publish` neu veröffentlicht wird, kennt es seine ursprüngliche Kategorie —
  unabhängig von Config-Defaults auf dem aktuellen System.

#### Archiv-Layout

Im Archiv-Ziel liegen pro Video **drei Dateien nebeneinander**:

```
nas:/Archiv/2026/Videoschnitt/
  2026-04-15-Mika-Socken-waschen.mov           ← Originalvideo (direkt, kein TAR)
  2026-04-15-Mika-Socken-waschen.settings.toml ← Management-Metadaten
  2026-04-15-Mika-Socken-waschen.archive.toml  ← konservierte Originalmetadaten
```

Vorteil: **direkter Zugriff auf archivierte Videos** am NAS — kein Entpacken
mehr nötig. Bestehende TAR-Archive aus früheren Versionen bleiben erhalten.

**`.archive.toml` hält fragile Werte fest**, die bei Moves zwischen Archiv-
Locations oder bei der ASCII-Slug-Benennung verloren gehen würden:

```toml
schema_version = 1
original_filename = "2026-04-15 Mika Socken waschen.mov"
original_mtime = 2026-04-15T18:22:31+02:00   # Export-/Schnittdatum (wichtig!)
archived_at = 2026-04-24T09:00:00+02:00
recording_date = 2026-04-15
title = "Mika Socken waschen"
description = """
Mehrzeilige Beschreibung möglich.
Umlaute, Quotes und Backslashes werden TOML-sicher escaped.
"""
```

Der `.settings.toml`-Sidecar bleibt das Publish-Management (Fingerprints für
Idempotenz, Poster-Einstellungen, per-Kanal-Timestamps) — `.archive.toml` ist
die Originalmetadaten-Konservierung. Andere Halbwertszeit, andere Datei.

#### Per-Kanal `published_at` und Partial-Failure-Schutz

Die drei `published_at`-Felder werden **unabhängig voneinander** gesetzt — ein
Feld erscheint erst, wenn der zugehörige rclone-Deploy erfolgreich war. Beispiel
einer typischen „Web ok, NAS war im Sleep"-Situation:

```toml
[web]
web_id = "a3xk9mn2pqrw"
published_at = "2026-04-18T10:23:45"   # Hostpoint erfolgreich

# [local] und [archive] fehlen komplett — NAS war offline
```

Beim nächsten `publish-dir`-Lauf erkennt der Skip-Check, dass Local und Archive
noch nicht publiziert sind, und baut das Video komplett neu auf (inkl. erneuter
Web-Publikation — `--force` auf Kanal-Ebene folgt in einer späteren Version).
Vorher wurde nur ein einziges `published_at` geschrieben, und der Skip-Check
interpretierte das als „fertig" → Infuse und Archive wurden nie nachgeholt.

### Category-Auflösung

Die Kategorie wird beim Publish in folgender Reihenfolge aufgelöst (erste Quelle mit Wert gewinnt):

1. **CLI `--category`** — expliziter Wille, überschreibt alles
2. **Video-Sidecar `[metadata].category`** — für aus dem Archiv extrahierte Videos
3. **`familienfilm.toml`** — Verzeichnis-Settings, nächstgelegenes Verzeichnis gewinnt (siehe [unten](#verzeichnis-settings-familienfilmtoml))
4. **Config `defaults.category`** — globaler Fallback
5. **ffprobe-Metadata** — QuickTime-Tag (LumaFusion-Exports liefern das typischerweise nicht)

Sidecar hat bewusst Vorrang vor Config und Directory-Settings: Ein Video soll seine
ursprüngliche Kategorie behalten, auch wenn der aktuelle Config-Default inzwischen
auf etwas anderes zeigt, oder der Export-Ordner zwischendurch eine andere `familienfilm.toml`
bekommen hat.

---

## Verzeichnis-Settings (`familienfilm.toml`)

Eine Datei namens `familienfilm.toml` in einem Quellverzeichnis liefert Default-Werte
für alle Videos darunter.

Gelesene Felder:

- **`category`** (string) — Kategorie für Videos im Verzeichnis.
- **`web_enabled`** (bool, seit v0.8.0) — **Opt-in für die Web-Veröffentlichung.**
  Seit v0.8.0 ist die Web-Publikation per Default **aus**. Nur Verzeichnisse
  mit explizitem `web_enabled = true` (oder Sidecar-Override, oder CLI `--web`)
  publizieren ins Web. Damit kann keine Datei mehr versehentlich veröffentlicht
  werden — insbesondere nicht beim Re-Publish aus dem Archiv. Der aufgelöste
  Wert wird beim Publish zusätzlich ins `.settings.toml`-Sidecar
  (`[web].enabled = true`) persistiert, damit die Entscheidung auch ohne
  Zugriff auf die Quell-`familienfilm.toml` erhalten bleibt.

### Format

```toml
# /Volumes/Videoschnitt/Luma Fusion-Export/Familie Kurmann-Glück/familienfilm.toml
category = "Familie Kurmann-Glück"
web_enabled = true   # Web-Kanal aktiv (Default ist aus)
```

```toml
# /Volumes/Videoschnitt/Luma Fusion-Export/Kurmann-Glück privat/familienfilm.toml
category = "Kurmann-Glück privat"
# kein web_enabled → Web bleibt aus (Safety-Default)
```

Keine hidden-Datei (kein führender Punkt) — absichtlich, damit sie beim Navigieren
sichtbar bleibt.

### Vererbung

Beim Publish jedes Videos wird die Verzeichniskette **von der Quelldatei nach oben**
gelaufen. **Pro Feld** gewinnt das nächstgelegene Vorkommen — verschiedene Felder
dürfen in verschiedenen Ebenen stehen (z.B. `category` oben, `skip_web` weiter
unten). Unterverzeichnisse überschreiben also Parents, pro Feld.

**Beispiel**:

```
/Movies/
  familienfilm.toml              ← category = "Familie Kurmann-Glück"
  2026-Frühjahr/
    2026-04-14 Ostern.mov        ← erbt "Familie Kurmann-Glück"
  Arbeit-Präsentationen/
    familienfilm.toml            ← category = "Firma XY"
    2026-04-15 Quartalsreport.mov ← "Firma XY" (Child-Override)
```

### Grenze der Suche

- Bei **`publish-dir`** und **`watch-dir`** ist der Scan-Root des Befehls die obere
  Grenze. Eine `familienfilm.toml` direkt im Scan-Root-Argument greift; darüber wird
  nicht gesucht.
- Beim Einzel-**`publish`** läuft die Suche bis zum Filesystem-Root — praktisch heisst
  das: bis zu der Datei, die du tatsächlich irgendwo im Baum hinterlegt hast.

### Rekursiver Scan (`--recursive` / `-r`)

`publish-dir` und `watch-dir` sind standardmässig **flach** (nur direkte Dateien
des Verzeichnisses). Mit `--recursive` werden auch Unterverzeichnisse durchsucht:

```bash
# Ganzer Jahresbaum in einem Zug:
familienfilm-manager publish-dir /Movies/LumaFusion-Export --recursive

# Watch über verschachtelte Verzeichnisse:
familienfilm-manager watch-dir -d /Movies/LumaFusion-Export --recursive
```

Hidden-Verzeichnisse (`.Trashes`, `.Spotlight-V100` etc.) werden übersprungen.
Sortierung läuft über Verzeichnisgrenzen hinweg (älteste Videos zuerst, egal in
welchem Unterverzeichnis).

Kombiniert mit `familienfilm.toml`: Du kannst einen Kategorien-Baum anlegen, jede
Ebene ihre eigene Kategorie — und trotzdem mit einem einzigen `publish-dir -r`-Aufruf
alles konsistent publizieren.

Bei einem erneuten `publish` werden die Werte automatisch aus der Sidecar-Datei gelesen – CLI-Parameter überschreiben gespeicherte Werte. Die Web-ID wird nach erfolgreicher Erstellung automatisch gespeichert, damit bei erneutem Publish die gleiche ID wiederverwendet wird (gleiche URL bleibt stabil).

Die Sidecar-Datei wird bei der Archivierung automatisch mit ins ISO aufgenommen.

**Deaktivieren:** `--no-sidecar` (pro Aufruf) oder `familienfilm-manager config set sidecar.enabled false` (global).

---

## API-Keys (KI-Funktionen)

Der Familienfilm-Manager selbst spricht **nicht** mit Claude. Wenn aber kein
Vorschaubild manuell gewählt wurde (kein `--poster-frame`/`--poster-at`,
kein Filename-Suffix, kein Sidecar-Wert), übergibt er die Auswahl an den
[`mediaset-creator`](https://github.com/kurmann/mediaset-creator), der
seinerseits den [`vorschaubild-manager`](https://github.com/kurmann/vorschaubild-manager)
nutzt — und letzterer ruft Claude auf, um aus einem 20-Frame-Mosaik das
beste Frame plus Bildausschnitt zu wählen.

### Empfohlenes Setup: `ANTHROPIC_API_KEY` als Umgebungsvariable

```bash
# In ~/.zshenv eintragen (gilt für interaktive UND non-interaktive Shells,
# also auch für launchd / cron / Tool-Subprozesse via uv tool):
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshenv

# Für laufende launchd-Jobs zusätzlich (einmalig, persistiert über Reboots):
launchctl setenv ANTHROPIC_API_KEY "sk-ant-..."
```

`ANTHROPIC_API_KEY` ist der **offizielle** Variablenname des Anthropic-SDK
und wird von allen meinen Tools bevorzugt gelesen. Der historische Name
`CLAUDE_API_KEY` bleibt als Fallback unterstützt.

### Wer liest den Key wann?

| Tool | Liest Key? | Wann |
|------|-----------|------|
| Familienfilm-Manager | ❌ nein | reicht nichts weiter, KI greift implizit auf das Prozess-ENV zu |
| Mediaset-Creator | ❌ nein | dito (Pass-Through) |
| Vorschaubild-Manager | ✅ ja | beim Mosaic-Auto-Frame und beim Crop-Auto-Wahl |

### Alternative: lokale Config-Datei

Falls du die Variable nicht ins ENV setzen willst:
```bash
vorschaubild-manager config set claude.api_key "sk-ant-..."
# → schreibt nach ~/.config/vorschaubild-manager/config.toml
```
Funktioniert auch, wenn der `vorschaubild-manager` nur als Library
(via Familienfilm-Manager-Dependency) installiert ist — der Pfad ist
unabhängig von der CLI-Nutzung.

### Ohne API-Key

Wenn weder ENV noch Config gesetzt sind und kein manuelles Vorschaubild
spezifiziert wurde, schlägt der Publish mit einer eindeutigen Fehlermeldung
aus dem Vorschaubild-Manager fehl. Lösung: entweder Key setzen oder
Vorschaubild explizit angeben (`--poster-at 47.5` o.ä.).

---

## Konfiguration

Einstellungen werden in `~/.config/familienfilm-manager/config.toml` gespeichert.

### Befehle

```bash
# Wert speichern
familienfilm-manager config set <schlüssel> "<wert>"

# Einzelnen Wert lesen
familienfilm-manager config get <schlüssel>

# Alle gespeicherten Werte anzeigen
familienfilm-manager config list
```

### Erlaubte Schlüssel

| Schlüssel | Beschreibung | Standard |
|-----------|--------------|---------|
| `targets.infuse` | rclone-Ziel für Infuse (z.B. `nas:/media/videos/`) | *(leer)* |
| `targets.web` | rclone-Ziel für Webserver (z.B. `azure:container/shares/`) | *(leer)* |
| `targets.archive` | rclone-Ziel für Archiv (z.B. `nas:/archive/familienfilme/`) | *(leer)* |
| `infuse.group_by` | Gruppierung: `year` oder `month` | `year` |
| `defaults.category` | Standard-Kategorie für Videos | *(leer)* |
| `sidecar.enabled` | Sidecar-Dateien automatisch lesen/schreiben | `true` |
| `cleanup.web_workdir` | Temporäres Web-Arbeitsverzeichnis nach erfolgreichem Deployment löschen | `true` |
| `paths.work_dir` | Standard-Arbeitsverzeichnis für temporäre Dateien (lokaler SSD bei SMB-Quellen empfohlen) | *(leer)* |
| `watch.directories` | Zu überwachende Verzeichnisse (komma-separiert) — siehe `watch-dir` | *(leer)* |
| `watch.interval_seconds` | Intervall zwischen Watch-Ticks in Sekunden | `86400` (24h) |
| `watch.stability_window_seconds` | Wartezeit zur Erkennung wachsender Dateien (0 = aus) | `60` |
| `mediaset.branding_base_url` | Stamm-URL für Web-Mediasets und OG-Tags | *(leer)* |
| `mediaset.branding_site_name` | Site-Name (sichtbar im Header + OG-Meta) | *(leer)* |
| `tools.ffmpeg` | Pfad zur ffmpeg-Binärdatei | `ffmpeg` |
| `tools.ffprobe` | Pfad zur ffprobe-Binärdatei | `ffprobe` |
| `tools.rclone` | Pfad zur rclone-Binärdatei | `rclone` |

### Beispiel

```bash
familienfilm-manager config set targets.infuse "nas:/media/familienfilme/"
familienfilm-manager config set targets.web "azure:kurmann-glueck/shares/"
familienfilm-manager config set targets.archive "nas:/archive/familienfilme/"
familienfilm-manager config set defaults.category "Familie Kurmann-Glück"
familienfilm-manager config set mediaset.branding_base_url "https://kurmannmedia.blob.core.windows.net/kurmann-glueck/"
```

### Arbeitsverzeichnis bei SMB-Quellen

Wenn die Quellvideos auf einem SMB-/NAS-Laufwerk liegen, sollte ein lokales
Arbeitsverzeichnis konfiguriert werden, damit Komprimierung und temporäre
Dateien nicht über das Netzwerk laufen:

```bash
familienfilm-manager config set paths.work_dir "~/Movies/.familienfilm-tmp"
```

Dann werden bei jeder Publikation:
- Quellvideo gelesen vom SMB
- Sidecar `.settings.toml` geschrieben auf SMB (klein)
- Komprimierung, Thumbnails, Web-Output: lokal im Work-Dir
- Web-Deployment: rclone push vom lokalen Work-Dir zum Webserver

Resolution: CLI `--work-dir` > Config `paths.work_dir` > Quellverzeichnis.

---

## Verwendung (Python API)

```python
from pathlib import Path
from familienfilm_manager.api import (
    PublishRequest,
    RuntimeOptions,
    publish,
)

request = PublishRequest(
    source_path=Path("/pfad/zu/video.m4v"),
    title="Wanderung auf den Napf",
    category="Familie Kurmann-Glück",
    recording_date="2025-03-24",
)

runtime = RuntimeOptions(
    infuse_target="nas:/media/familienfilme/",
    web_target="azure:kurmann-glueck/shares/",
    archive_target="nas:/archive/familienfilme/",
    branding_base_url="https://example.com/shares/",
)

result = publish(request, runtime)

if result.success:
    print(f"Infuse: {result.infuse_dir} (deployed: {result.infuse_deployed})")
    print(f"Web: {result.web_dir} (deployed: {result.web_deployed})")
    print(f"Archiv: {result.archived}")
else:
    print(f"Fehler: {result.error_message}")
```

### Öffentliche API-Exporte

```python
from familienfilm_manager.api import (
    publish,                 # Gesamtworkflow
    publish_directory,       # Batch über ein Verzeichnis
    watch_directories,       # Endlosschleife / Single-Shot über mehrere Verzeichnisse
    deploy_infuse,           # Nur Infuse-Deployment
    deploy_web,              # Nur Web-Deployment
    archive,                 # Nur Archivierung
    PublishRequest,          # Fachlicher Request
    PublishResult,           # Ergebnis
    DirectoryPublishResult,  # Batch-Ergebnis
    VideoStatus,             # Status eines Videos im Batch
    VideoStatusKind,         # PUBLISHED/SKIPPED/FAILED/PENDING/UNSTABLE
    WatchRequest,            # Watch-Request
    WatchResult,             # Watch-Gesamtergebnis
    WatchTickResult,         # Ergebnis eines einzelnen Ticks
    DeployRequest,           # Deploy-Request
    DeployResult,            # Deploy-Ergebnis
    ArchiveRequest,          # Archiv-Request
    ArchiveResult,           # Archiv-Ergebnis
    RuntimeOptions,          # Technische Optionen
    FamilienfilmEvent,       # Fortschritts-Event
    FamilienfilmStage,       # Stage-IDs
)
```

---

## Lizenz

MIT
