Metadata-Version: 2.4
Name: kurmann-familienfilm-manager
Version: 0.6.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.0.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.

---

## 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 Original + Sidecars als TAR (Python tarfile → rclone move)

### 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 |
| `--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) |
| `--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.
- **TAR-Datei bleibt bei Archive-Deploy-Fehler erhalten** (neben dem Quellvideo).
  Nachholen:
  ```bash
  rclone move <pfad/zum/tar> <archive-target>
  ```

---

## 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
```

### 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).

### HTML-Benachrichtigung mit Live-Status

Ist `notification.file` konfiguriert, wird bei Publikationsbeginn ein
„wird publiziert…"-Eintrag geschrieben und beim Abschluss durch den finalen
Zustand ersetzt:

- **wird publiziert…** — orange, während der Publikation
- **publiziert** — mit Link zur Web-URL
- **fehlgeschlagen** — rot, mit Fehlermeldung
- **abgebrochen** — grau (wird beim Watch-Start gesetzt, falls vom letzten
  Lauf noch pending-Einträge liegenblieben, z.B. nach einem Crash)

Jeder Eintrag hat eine stabile `data-video-id` (aus dem absoluten Pfad
abgeleitet) — ein Republish überschreibt denselben Eintrag statt Duplikate
anzuhängen.

### 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. |
| `--work-dir PATH` | Arbeitsverzeichnis (überschreibt `paths.work_dir`). |
| `--verbose`, `-v` | Zusätzliche Ablaufinformationen auf stderr. |

---

## Metadaten-Extraktion

Metadaten werden in folgender Priorität ermittelt:

### Aufnahmedatum

1. **CLI-Parameter** (`--date`) – überschreibt alles
2. **QuickTime-Tag** (`com.apple.quicktime.creationdate`) – bewusst in Final Cut Pro gesetztes Datum
3. **Dateiname** – ISO-Datum am Anfang des Dateinamens (z.B. `2019-12-22 Twannbachschlucht.mov`)
4. **Generische Tags** (`creation_time`, `date`) – oft das Datei-Änderungsdatum, nicht das Aufnahmedatum

Der Dateiname hat bewusst höhere Priorität als generische Tags (`creation_time`), weil der Videoschnitt selten am gleichen Tag wie die Aufnahme stattfindet. So kann das korrekte Aufnahmedatum über den Dateinamen bestimmt werden.

### 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** (TAR im Langzeit-Archiv)
- **`[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 ab v0.6.0

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

```
nas:/Archiv/2026/Videoschnitt/
  2026-04-15-Mika-Socken-waschen.tar           ← Video + evtl. .fcpxml/.lfpackage
  2026-04-15-Mika-Socken-waschen.settings.toml ← Management-Metadaten
```

Damit sind **Archiv-Metadaten direkt per `rclone ls` / Finder sichtbar** —
z.B. welche Kategorie, wann publiziert, welche Web-ID. Ohne das TAR entpacken
zu müssen.

Das TAR selbst enthält **nur Video + Produktions-Nebenmaterialien** (wie
`.fcpxml`, `.lfpackage`, Fanart-Dateien, NFO). Der Management-State
(`.settings.toml`) bleibt ausserhalb — semantisch sauber entkoppelt
(der `[archive].published_at`-Zeitstempel würde sonst zirkulär im Archiv
liegen, das er beschreibt).

**TAR-Erstellung**: Das TAR wird im konfigurierten `paths.work_dir` erstellt,
nicht mehr im Quellverzeichnis. Bei SMB-/NAS-Quellen spart das 1–7 GB
Netzwerk-Schreibvorgang pro Video und vermeidet SMB-Sync-Delays vor dem
finalen `rclone move`. Ohne `paths.work_dir` bleibt das Fallback auf das
Quellverzeichnis (rückwärtskompatibel).

#### 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. Aktuell wird nur `category` gelesen — die Struktur ist so
angelegt, dass weitere Felder (z.B. `author`) ohne Breaking Change ergänzt werden können.

### Format

```toml
# /Movies/LumaFusion-Export/familienfilm.toml
category = "Familie Kurmann-Glück"
```

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. Die **nächstgelegene** `familienfilm.toml` mit einem gesetzten Feld gewinnt.
Unterverzeichnisse überschreiben also Parents.

**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).

---

## 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
