Metadata-Version: 2.4
Name: kurmann-kamera-einleser
Version: 2.3.0
Summary: Einfacher & solider Medien-Importeur. Archiviert Videoaufnahmen von SSD/iPhone/NAS in ISO-Dateien.
Project-URL: Homepage, https://github.com/kurmann/kamera-einleser
Project-URL: Repository, https://github.com/kurmann/kamera-einleser
Project-URL: Changelog, https://github.com/kurmann/kamera-einleser/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/kurmann/kamera-einleser/issues
Author: Patrick Kurmann
License-Expression: MIT
License-File: LICENSE
Keywords: archive,backup,import,iso,media,nas,rclone,synology
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Archiving :: Backup
Requires-Python: >=3.10
Requires-Dist: inquirerpy>=0.3.4
Requires-Dist: kurmann-medien-leser<0.2.0,>=0.1.0
Requires-Dist: rich>=13.0.0
Requires-Dist: tomli-w>=1.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Requires-Dist: typer>=0.9.0
Provides-Extra: dev
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# 🎬 Kamera-Einleser

Einfacher & solider Medien-Importeur. Archiviert Videoaufnahmen von SSD/iPhone in ISO-Dateien (organisiert nach echtem Aufnahmedatum) und synchronisiert sie auf Cloud-Speicher.

> **2.0.0 ist ein Major-Release** mit zwei strukturellen Verbesserungen:
> - **Manifest-System pro ISO** mit reichhaltigen Metadaten (Aufnahmedatum mit Provenance, Kamera, GPS, Audio/Video-Codec, Production-Felder), vereinheitlicht über Blackmagic Camera, iPhone Camera und Final Cut Camera
> - **Build-Strategie auf `recorded_at`**: Files werden nach echtem EXIF-Aufnahmedatum gruppiert, nicht mehr nach Filesystem-`mtime`. Damit entstehen keine "Misch-Tag-ISOs" mehr.
>
> Breaking-Changes und Migrations-Pfad: siehe [CHANGELOG.md](CHANGELOG.md).

## ✨ Features

- **🗜 ZIP-Archivierung (ab 2.2.0)**: Erstellt `ZIP_STORED`-Archive (ohne Kompression). Streaming-Bau ohne Staging-Zwischenkopie spart pro Tag eine komplette Source-Grösse an Disk-Footprint.
- **📦 Append in bestehende ZIPs (ab 2.2.0)**: Kommen für einen Tag innerhalb der Cache-Window neue Files dazu, wird das bestehende ZIP in-place erweitert (Stream-Copy alter Members + neue Files dazu). Kein `-Teil-N` mehr — ein ZIP pro Tag.
- **🛡️ Lazy-Check vor Append**: Vor jedem Server-überschreibenden Upload wird das Server-Manifest gepullt (sub-Sekunde, KBs) und mit dem geplanten neuen Inhalt verglichen. Würde ein Datei verloren gehen, blockt der Lauf hart.
- **📅 Tag-Gruppierung nach echtem Aufnahmedatum**: Files werden anhand des EXIF-`recorded_at` zugeordnet (ab 2.0.0). Ein Misch-Tag-Bug aus 1.x ist damit strukturell gelöst.
- **🔄 Idempotente Archivierung**: Prüft vor Erstellung, ob Dateien bereits im Archiv vorhanden sind. Funktioniert format-agnostisch für `.iso` (Legacy) und `.zip`.
- **☁️ rclone-Integration**: Synchronisiert Archive zu beliebig vielen Cloud-Zielen (NAS, Mega.nz, Google Drive, etc.) mit dynamischem `--immutable`-Schutz.
- **♻️ Cache-Variante-C (ab 2.2.0)**: ZIPs der letzten N Tage (Default 7) bleiben lokal für schnellen Append, ältere werden auto-prunt — sofern repliziert (`*.uploaded.json`-Marker bestätigt).
- **🔗 SMB-Mount**: Mounte Archive direkt via SMB ohne Download.
- **📥 Archiv-Wiederherstellung**: Lädt Archiv vom NAS herunter und mountet es automatisch.
- **🔍 Intelligente Suche**: Findet Archive nach Namen mit Pagination und Filterung.
- **🤖 Headless-Modus**: Automatische Wiederherstellung/Mount per Kommandozeile.
- **⚙️ Interaktive Konfiguration**: Benutzerfreundliche Einstellungsverwaltung.
- **📂 Mehrere Quellen**: Sequenzielle Verarbeitung mehrerer Quellverzeichnisse.
- **🗑️ Optionale Löschung**: Fragt am Ende nach dem Löschen der Originaldateien.
- **📑 Reichhaltige Manifests pro Archiv**: jedes Archiv trägt automatisch ein JSONL-Sidecar mit pro-Datei-Metadaten — Aufnahmedatum mit Provenance, Kamera-Modell, GPS, Codec, Apple-ProApps-Production-Felder. Die Metadaten-Logik lebt im separaten Paket [`kurmann-medien-leser`](https://github.com/kurmann/medien-leser) (ab 2.1.0).
- **🔍 Audit-Befehl**: `kamera-einleser audit iso-grouping` zeigt Misch-Tag-Archive (alte ISOs aus 1.x mit mtime-Gruppierung).
- **♻️ Recovery ohne erneuten Pull**: `kamera-einleser cache resync` schickt unvollständig replizierte Cache-Archive an die fehlenden Targets nach.

## 🎬 Unterstützte Aufnahme-Quellen (ab 2.0.0)

Eines der zentralen Features: **kamera-einleser vereinheitlicht die Metadaten aus verschiedenen iPhone-Kamera-Apps in ein einziges, konsistentes Manifest-Schema**. Jeder App-Hersteller schreibt EXIF-Tags in seine eigene Stelle und mit eigenen Konventionen — der Indexer kennt die Eigenheiten und produziert für alle drei Quellen dasselbe Schema, mit klarer Provenance über die Quelle und das Aufnahmedatum.

| App | Aufnahmedatum | Eigenheiten | Reiche Metadaten |
|---|---|---|---|
| **Blackmagic Camera** | `Keys:CreationDate` mit TZ ✓ | viele proprietäre Felder (`Blackmagic-design:*`) | Apple ProApps Production-Felder (Reel/Scene/Shot/Project/Director), Kamera-Settings (ISO, Shutter, WB, Aperture) |
| **iPhone Camera** (Apple Foto-App) | `Keys:CreationDate` mit TZ ✓ | Standard Apple QuickTime-Tags | Lens-Modell (`VideoKeys:LensModel`), GPS mit Höhe |
| **Final Cut Camera** (Apple, iOS 18+) | **kein** `CreationDate` — nur `CreateDate` (UTC ohne TZ) ⚠️ | identifizierbar via `AppleProappsAppBundleID = com.apple.FinalCutApp.companion` | Lens-Modell, GPS mit Höhe, App-Version |

**Wichtigste Vereinheitlichungs-Tricks:**

1. **Tag-Pfad-Aggregation**: Tag-Werte können je nach App in unterschiedlichen EXIF-Atomen liegen (z.B. `Keys:Make` bei iPhone vs. `QuickTime:Make` bei BMD). Der Indexer schaut alle bekannten Pfade in priorisierter Reihenfolge an.

2. **Format-Normalisierung**: exiftool liefert Datums oft als `2026:04:20 12:52:15` (mit `:` als Datums-Trenner). Der Indexer normalisiert auf saubere ISO-8601 (`2026-04-20T12:52:15+02:00`), damit Konsumenten konsistent parsen können.

3. **Zeitzonen-Inferenz für Final Cut Camera**: FCC schreibt `CreateDate` in **UTC ohne TZ-Marker**. Wenn das als-ist gelesen wird, ist die Aufnahmezeit 1-2 Stunden falsch. Der Indexer leitet die Zeitzone aus `file_mtime` (lokal mit TZ) ab und setzt sie auf den UTC-Wert auf. `recorded_at_source` zeigt dann `exif:CreateDate+tz_from_mtime`, damit transparent ist, dass die TZ inferiert wurde.

4. **Datums-Provenance**: jedes Manifest-Entry hat ein `recorded_at` (Best-Guess) und ein `recorded_at_source` (welche Quelle wurde gewählt). Bei Misstrauen kann der Konsument im `dates`-Subblock alle Roh-Quellen einsehen und selbst entscheiden.

5. **Source-App-Detection**: jede Datei kriegt im Manifest ein `source_app`-Feld mit "Blackmagic Camera", "iPhone Camera" oder "Final Cut Camera" — falls die Quelle eindeutig erkannt werden konnte. Andere Apps funktionieren oft auch, kriegen aber `source_app=null` und können einzelne Felder vermissen.

**Beispiel-Output aus einem Final Cut Camera-File:**

```json
{
  "filename": "IMG_0001.mov",
  "recorded_at": "2026-05-03T07:38:55+02:00",
  "recorded_at_source": "exif:CreateDate+tz_from_mtime",
  "source_app": "Final Cut Camera",
  "camera": {
    "make": "Apple",
    "model": "iPhone 15 Pro Max",
    "software": "26.4.2",
    "lens": "iPhone 15 Pro Max back camera 6.765mm f/1.78"
  },
  "gps": {"lat": 47.07054, "lon": 7.58165, "altitude_m": 516.0},
  "video": {"codec": "HEVC", "resolution": "3840x2160", "fps": 30}
}
```

## 📋 Voraussetzungen

- macOS (für ISO-Erstellung via hdiutil)
- Python 3.10 oder höher
- [rclone](https://rclone.org/) (für Cloud-Synchronisation)
- [exiftool](https://exiftool.org/) (für Manifest-Metadaten ab 2.0.0): `brew install exiftool`

## 🚀 Installation

### Empfohlen: Mit uv

```bash
# uv installieren (falls noch nicht vorhanden)
curl -LsSf https://astral.sh/uv/install.sh | sh

# oder mit brew
brew install uv

# kamera-einleser installieren
uv tool install git+https://github.com/kurmann/kamera-einleser.git
```

### Alternative: Mit pip / pipx / uv

Von PyPI (empfohlen ab 1.4.0):

```bash
uv tool install kurmann-kamera-einleser
# oder
pipx install kurmann-kamera-einleser
# oder
pip install kurmann-kamera-einleser
```

Direkt vom Git-Repository (für Entwicklung):

```bash
pip install git+https://github.com/kurmann/kamera-einleser.git
```

### rclone installieren

```bash
# macOS
brew install rclone
```

## 🎯 Schnellstart

### 1. Einstellungen konfigurieren

```bash
kamera-einleser settings
```

Hier konfigurierst du:
- **Quellverzeichnisse**: Wo liegen deine Kamera-Aufnahmen?
- **rclone-Ziele**: Wohin sollen die Backups synchronisiert werden?
- **Archiv-Verzeichnisse**: Wo sind deine Archive gespeichert? (für Wiederherstellung)
- **Arbeitsverzeichnis**: Wo werden ISO-Dateien lokal erstellt/gespeichert?
- **Ausschluss-Muster**: Dateien/Ordner, die nicht archiviert werden sollen

### 2. Archivierung starten

```bash
# Archiviert alle konfigurierten Quellverzeichnisse
kamera-einleser archive

# Oder direkt ein bestimmtes Verzeichnis angeben
kamera-einleser archive /Volumes/SSD/Videos

# Mit optionaler Löschung der Originale am Ende
kamera-einleser archive --delete-source
```

### 2a. Arbeitsverzeichnisse (primär + Fallback)

Ab **1.4.0** kann man zwei Arbeitsverzeichnisse konfigurieren. Das Tool nimmt automatisch das passende:

```bash
# Primär: interne SSD — schnell, ideal für kleine/mittlere Volumina
kamera-einleser config set working_directory ~/Movies/Kamera-Einleser

# Fallback: externe SSD — gross, ideal für 100 GB+ Pulls vom NAS
kamera-einleser config set working_directory_fallback /Volumes/Samsung2TB/Kamera-Einleser
```

#### Split-Pipeline (ab 1.6.0) — Pull und ISO auf verschiedene Volumes

Sind **beide** Arbeitsverzeichnisse verfügbar und haben je genug Platz, wird die
Pipeline automatisch gesplittet:

- **Pull → Primary** (interne SSD, schneller Lesepfad für den rclone-Download)
- **ISO → Fallback** (externe SSD, sequentieller Schreibpfad fürs Image)

Vorteile:

- Jedes Volume braucht nur **~1.5× Quell-Grösse** frei statt zusammen 3.5×.
- Lese- und Schreibpfad auf verschiedenen Disks → entkoppelte I/O, potenziell schneller.
- Staging landet wie bisher im System-Temp (`/var/folders` auf macOS).

Ist nur ein Volume verfügbar oder hat nur eins genug Platz, fällt der Resolver
auf **Single-Volume-Modus** zurück (alles auf einer Disk mit 3.5× Footprint).

#### Resolver-Logik pro Quelle

Der Resolver entscheidet pro Quelle und wählt in dieser Reihenfolge:

1. **Split-Modus**: beide Volumes mounted, je ≥ `Quelle × 1.5` frei →
   Pull→Primary, ISO→Fallback.
2. **Single-Volume-Modus**: ein Volume mit voller Pipeline-Kapazität
   (Quelle × 3.5 bei rclone, × 2.5 bei lokal).
3. **Fallback bei lokaler Quelle**: konfiguriertes Working-Dir oder System-Temp.
4. **Fehler bei rclone-Quelle**: harter Abbruch mit klarer Meldung — Temp
   kann auf macOS im RAM liegen und einen 200-GB-Pull sprengen.

### 2a-bis. Lokaler Archiv-Cache (Variante C, ab 2.2.0)

Die erstellten Archive bleiben nach erfolgreichem Upload im Arbeitsverzeichnis
liegen. Das ist **kein echtes Backup mit Garantien** — die echten Sicherungen
sind deine rclone-Targets (NAS, Cloud). Der lokale Cache dient zwei Zwecken:

1. **Schnelle Subset-Checks** (Fall A): das Tool öffnet das lokale ZIP/ISO
   direkt, statt den Mount-Roundtrip via `hdiutil` zu fahren oder das Archiv
   vom Server zu pullen.
2. **Append-Window**: solange das ZIP eines Tages im Cache ist, können neue
   Files für denselben Tag in dieses ZIP hineinwachsen — kein `-Teil-N` mehr.

**Variante C — zwei orthogonale Limits**:

- **`iso_cache_max_days = 7`** (Default): nach 7 Tagen rückwärts (gemessen am
  `day_key`) fliegen Archive aus dem Cache. Die Server-Kopie bleibt natürlich
  unangetastet. Tag-Limit `0` deaktiviert dieses Verhalten.
- **`iso_cache_max_bytes = 500 GB`** (Default): Belt-and-Suspenders gegen
  Disk-Überlauf. Falls nach dem Tag-basierten Prune die Cache-Summe noch zu
  hoch ist, fallen die ältesten replizierten Archive zusätzlich. `0` =
  unlimitiert.

**Safety**: nur Archive mit vollständigem `*.uploaded.json`-Marker (= auf
allen aktuell konfigurierten Targets repliziert) werden gelöscht. Archive
ohne Marker bleiben in jedem Fall liegen.

```bash
# Cache-Window anpassen
kamera-einleser config set iso_cache_max_days 14         # zwei Wochen lokal
kamera-einleser config set iso_cache_max_bytes 1000000000000  # 1 TB Backstop

# Manuell aufräumen (Tag-basiert)
kamera-einleser cache list
kamera-einleser cache prune --dry-run         # Defaults aus Config
kamera-einleser cache prune --max-days 7
kamera-einleser cache prune --max-size-gb 500

# Einen einzelnen Archive-Lauf overriden
kamera-einleser archive --no-prune-cache   # Cache diesmal in Ruhe lassen
kamera-einleser archive --prune-cache      # Pruning erzwingen
```

### 2b. Netzlaufwerke als Quellen (rclone, z.B. Synology NAS)

Ab **1.4.0** dürfen Quellen auch rclone-Remotes sein — zum Beispiel ein
Synology NAS. Die Dateien werden beim Archive-Lauf zunächst temporär in das
Arbeitsverzeichnis gezogen (Staging) und von dort ganz normal in ISO-Dateien
pro Tag verpackt.

```bash
# Einmalig rclone für das NAS einrichten (interaktiv)
rclone config

# Quellen können ab jetzt lokale Pfade *oder* rclone-Specs sein:
kamera-einleser config get source_directories
# → ["/Volumes/Card", "synology:/volume1/Fotos/Eingang"]
```

Das Parsing erfolgt über das Muster `remote:pfad`. Lokale Pfade beginnen mit
`/` oder `~`. Auf NAS-Quellen verhält sich die CLI identisch zu lokalen
Quellen, mit Ausnahme von `--delete-source`: das wirkt nur auf lokale Quellen.

#### Manifest-Pre-Filter: wir pullen nur, was noch nicht archiviert ist

Beim rclone-Pull zieht der Tool **nicht jedes Mal alle Dateien** vom Netz-
laufwerk. Stattdessen:

1. Die lokalen `*.manifest.jsonl`-Sidecars im Cache werden gelesen — sie
   enthalten alle Dateinamen, die bereits in einem Archiv (ISO oder ZIP)
   stecken.
2. Vor dem Pull wird eine temporäre rclone-Filter-Datei erzeugt, die genau
   diese Dateinamen als Exclude-Patterns enthält.
3. rclone bekommt die Filter-Datei via `--filter-from` und überspringt die
   bereits archivierten Dateien **remote-seitig** — sie werden nie über das
   Netz übertragen.

Das macht wiederholte Läufe um Grössenordnungen schneller. Beispiel: bei einer
265-GB-NAS-Quelle, von der 95 % bereits archiviert sind, werden nur die
restlichen ~13 GB gezogen statt die vollen 265 GB.

**Warum filename-basiert und nicht checksum-basiert?** Nicht alle rclone-
Backends liefern server-seitige Hashes (SMB z.B. nicht). Filename-basiert
funktioniert dagegen auf jedem Backend und ist für typische
Kamera-Dateinamenskonventionen (z.B. BMPCC: `A001_04051010_C123.mov`
= Karte+Timecode+Clip-Nummer) effektiv eindeutig. Die zusätzliche
Fall-A-Subset-Check-Prüfung im Build-Schritt fängt Rest-Kollisionen ab,
falls doch zwei Dateien mit identischem Namen unterschiedlichen Inhalt
hätten.

**Pre-Filter umgehen (Debugging):**

```bash
kamera-einleser archive --full-pull
```

Zieht alle Dateien der Remote-Quelle erneut, unabhängig vom Manifest-Cache.

#### Startup-Guard: Schutz vor "Manifest-Cache leer, Remote voll"

Wenn beim `archive`-Lauf der lokale Manifest-Cache **leer** ist, aber auf
den konfigurierten rclone-Targets Archive **existieren**, bricht der Tool
mit klarer Fehlermeldung ab. Hintergrund: in diesem Zustand würde `archive`
alle Tage neu aufbauen und am `rclone --immutable`-Upload scheitern.

```
❌ Abbruch: kein lokales Manifest, aber auf den konfigurierten Targets
existieren 113 Archiv(e).

Empfohlen — Manifests vom Server holen:
    rclone copy <target>:Originalmedien /pfad/zum/cache --include '*.manifest.jsonl'
```

Übersteuern (nur bewusst, z.B. nach kompletter Remote-Löschung):
```bash
kamera-einleser archive --force
```

Mit `--no-upload` ist der Guard ohnehin deaktiviert (kein Netz-Zugriff).

#### Cross-Machine: Manifests sind selbst die Wahrheit

Bis 2.1.0 lebte parallel zu den Archiven eine zentrale `checksum_index.csv`,
die via `kamera-einleser index push/pull` zwischen Rechnern synchronisiert
wurde. Ab **2.2.0 ist diese Infrastruktur entfernt**:

- Jedes Archiv hat sein `*.manifest.jsonl`-Sidecar **direkt neben sich** auf
  dem Server (wandert beim Upload mit). Das Manifest enthält pro File
  SHA-256 + medien-leser-Metadaten — alles was der CSV-Index früher trug.
- Auf einem Zweit-Rechner reicht ein einmaliges `rclone copy` der Sidecars
  vom NAS in den lokalen Cache, und Pre-Filter + Fall-A-Subset-Check
  funktionieren ohne Wissen über fremde Maschinen.
- Der `*.uploaded.json`-Marker pro Archiv hält **maschinenlokal** fest, dass
  DIESE Maschine erfolgreich zu allen aktuell konfigurierten Targets
  repliziert hat — Voraussetzung für `cache prune`-Safety.

### 3. Archive mounten (ohne Download)

Mounte Archive direkt via SMB für schnellen, schreibgeschützten Zugriff:

```bash
# Interaktiver Modus: Wähle Archiv aus Liste
kamera-einleser mount

# Headless-Modus: Mounte direkt
kamera-einleser mount Originalmedien-2026-01-15.iso

# Oder mit Alias:
kamera-einleser load Originalmedien-2026-01-15.iso

# Zeige gemountete Archive
kamera-einleser mount --list-mounted
kamera-einleser status

# Unmounten
kamera-einleser unmount Originalmedien-2026-01-15.iso
# Oder alle:
kamera-einleser unmount --all
```

**Vorteile:**
- ✅ Kein lokaler Speicherplatz nötig
- ✅ Sofortiger Zugriff (kein Download)
- ✅ Ideal für Durchsuchen, Ansehen, Videoschnitt mit Referenzen

**Einrichtung:**
Füge `smb_url` zu deinen rclone-Zielen in der Config hinzu:

```bash
kamera-einleser settings  # → rclone-Ziele bearbeiten
```

In der Config-Datei (`~/.config/kamera-einleser/config.yaml`):
```yaml
rclone_targets:
  - name: "NAS"
    path: "nas:/Archiv"
    smb_url: "smb://user@nas.local/Archiv"  # Diese Zeile hinzufügen
```

Beim ersten Mount fragt macOS nach Zugangsdaten und bietet an,
diese im Schlüsselbund zu speichern. Danach erfolgen Mounts automatisch.

### 4. Archiv wiederherstellen

```bash
# Interaktiver Modus: Durchsuche und wähle Archive aus
kamera-einleser restore

# Headless-Modus: Automatische Wiederherstellung nach Dateiname
kamera-einleser restore Originalmedien-2026-01-15.iso

# Mit eigenem Zielverzeichnis
kamera-einleser restore --restore-dir /custom/path Originalmedien-2026-01-15
```

### 5. Optionale Parameter für Archivierung

```bash
# Nur Archiv erstellen, ohne Upload
kamera-einleser archive --no-upload

# Originaldateien nach erfolgreichem Backup löschen (fragt nach)
kamera-einleser archive --delete-source
```

## 📖 Befehle

### `settings`
Öffnet das interaktive Einstellungsmenü.

```bash
kamera-einleser settings
```

### `config`
Zeigt die aktuelle Konfiguration an.

```bash
kamera-einleser config
```

### `archive`
Erstellt tägliche Archive (ZIP ab 2.2.0, ISO als Legacy) und synchronisiert
sie zu Cloud-Zielen. Append in bestehende ZIPs des Cache-Windows.

```bash
kamera-einleser archive [QUELLVERZEICHNIS] [OPTIONEN]
```

**Optionen:**
- `--no-upload`: Nur Archiv erstellen, nicht synchronisieren
- `--delete-source`: Originaldateien nach erfolgreichem Backup löschen
- `--full-pull`: Manifest-Pre-Filter umgehen, alle Files erneut pullen
- `--force`: Startup-Guard (leerer Manifest-Cache + volle Remotes) übersteuern
- `--prune-cache` / `--no-prune-cache`: Auto-Prune des lokalen Caches
  am Ende des Laufs erzwingen bzw. überspringen

### `cache`
Operator-Befehle rund um den lokalen Archiv-Cache.

```bash
kamera-einleser cache list                       # alle lokalen Archive + Replikations-Status
kamera-einleser cache prune                      # Auto-Prune nach Config-Defaults
kamera-einleser cache prune --max-days 7         # Tag-basiert
kamera-einleser cache prune --max-size-gb 500    # Bytes-basiert
kamera-einleser cache prune --dry-run            # nur Plan, nichts löschen
kamera-einleser cache resync                     # nicht voll replizierte ZIPs erneut hochladen
```

### `manifest` / `audit`
Diagnose-Werkzeuge rund um die `*.manifest.jsonl`-Sidecars.

```bash
kamera-einleser manifest build <archiv>          # Manifest neben einer ISO/ZIP nachholen
kamera-einleser manifest show <manifest>         # Manifest-Inhalt anzeigen
kamera-einleser audit iso-grouping <pfad>        # Misch-Tag-Archive aus 1.x erkennen
```

### `restore`
Stellt ein ISO-Archiv aus Cloud-Speicher wieder her (mit Download).

```bash
# Interaktiver Modus
kamera-einleser restore

# Headless-Modus mit Dateiname
kamera-einleser restore [DATEINAME] [OPTIONEN]
```

**Optionen:**
- `--restore-dir, -r PATH`: Zielverzeichnis für Wiederherstellung (optional)

### `mount` / `load`
Mountet ein Archiv direkt via SMB ohne Download (schneller, schreibgeschützter Zugriff).

```bash
# Interaktiver Modus
kamera-einleser mount
kamera-einleser load  # Alias

# Headless-Modus mit Dateiname
kamera-einleser mount [ARCHIVNAME]

# Zeige gemountete Archive
kamera-einleser mount --list-mounted

# Zeige Mount-Pfad (für Scripting)
kamera-einleser mount --print-path [ARCHIVNAME]
```

**Voraussetzungen:**
- SMB-URL in `config.yaml` konfiguriert (siehe "Archive mounten" oben)
- Netzwerkzugriff auf das NAS

### `unmount`
Unmountet gemountete Archive.

```bash
# Interaktiver Modus (Auswahl aus Liste)
kamera-einleser unmount

# Headless-Modus mit Archivname
kamera-einleser unmount [ARCHIVNAME]

# Alle Archive unmounten
kamera-einleser unmount --all
```

### `status`
Zeigt alle aktuell gemounteten Archive an.

```bash
kamera-einleser status
```

### `version`
Zeigt die installierte Version an.

```bash
kamera-einleser --version
```

## ⚙️ Konfiguration

Die Konfigurationsdatei liegt unter:
```
~/.config/kamera-einleser/config.yaml
```

Beispiel-Konfiguration:

```yaml
source_directories:
  - /Volumes/SSD/BlackmagicCamera
  - /Volumes/iPhone/DCIM
rclone_targets:
  - name: NAS
    path: nas:/Backups/Videos/2026
  - name: Mega.nz
    path: mega:Backups/Videos
archive_directories:
  - nas:/Backups/Videos/2026
  - nas:/Backups/Videos/2025
  - mega:Backups/Videos
current_archive_directory: nas:/Backups/Videos/2026
working_directory: /Users/username/Movies/Kamera-Einleser
restore_directory: /Users/username/Movies/Kamera-Restore
excludes:
  - ._*
  - .DS_Store
```

**Konfigurationsoptionen:**
- `source_directories`: Liste von Quellverzeichnissen zum Archivieren
- `rclone_targets`: Liste von rclone-Zielen für Synchronisation
- `archive_directories`: Liste von rclone-Pfaden, die nach Archiven durchsucht werden
- `current_archive_directory`: Das aktuell verwendete Verzeichnis für neue Archive
- `working_directory`: Lokales Verzeichnis für ISO-Dateien (nicht temporär!)
- `restore_directory`: Standard-Verzeichnis für wiederhergestellte ISOs
- `excludes`: Muster für auszuschließende Dateien (z.B. `.DS_Store`, `._*`)

💡 **Tipp**: Das Arbeitsverzeichnis ist der lokale Speicher für ISO-Dateien. Diese werden mit rclone zu den Zielen synchronisiert und lokal behalten für Videoschnitt etc.

## 🔧 rclone einrichten

Bevor du Cloud-Ziele nutzen kannst, musst du rclone konfigurieren:

```bash
# Interaktive rclone-Konfiguration
rclone config

# Teste die Verbindung
rclone lsd nas:
```

Weitere Infos: [rclone.org/docs](https://rclone.org/docs/)

## 🎬 Workflow-Beispiele

### Archivierung

1. Kamera-SSD am Mac anschließen
2. `kamera-einleser archive` ausführen
3. Programm analysiert Dateien und gruppiert sie nach Tag (Änderungsdatum)
4. Prüft bestehende ISOs im Arbeitsverzeichnis und auf allen rclone-Zielen
5. Prüft Idempotenz: Überspringt Tage, deren Dateien bereits archiviert sind
6. Erstellt ditto-Staging für neue/geänderte Tage (Metadaten-erhaltend)
7. Erstellt ISO-Dateien mit UDF-Format (z.B. "Originalmedien-2026-01-15.iso")
8. Verifiziert die Integrität mit hdiutil
9. Synchronisiert zu allen konfigurierten Cloud-Zielen (organisiert in Jahresordner)
10. Optional: Löscht Originaldateien nach Bestätigung am Ende

### Wiederherstellung

**Interaktiv:**
1. `kamera-einleser restore` ausführen
2. Optional: Nach Dateinamen suchen
3. Durch Archive blättern (10 pro Seite)
4. Archiv auswählen
5. Bestätigen
6. Programm lädt ISO herunter und mountet es

**Headless (Automatisiert):**
```bash
# In Skript oder Cronjob
kamera-einleser restore Originalmedien-2026-01-15 --restore-dir /restore/path
```

## 🎯 Archivierungs-Konzept

Das Programm verwendet tägliche ISO-Archive:

1. **Datei-Analyse**: Scannt alle Dateien und liest deren Änderungsdatum
2. **Tägliche Gruppierung**: Gruppiert Dateien nach Tag (YYYY-MM-DD)
3. **ISO pro Tag**: 
   - Dateiname: `Originalmedien-YYYY-MM-DD.iso`
   - Volume-Name: `Originalmedien-YYYY-MM-DD`
   - Format: UDF (Universal Disk Format)
4. **Idempotenz**: 
   - Prüft vor Erstellung, ob alle Dateien bereits im bestehenden ISO vorhanden sind
   - Prüft bestehende ISOs auf allen konfigurierten rclone-Zielen (Namenskollision)
   - Überspringt Tag wenn bereits vollständig archiviert
   - Bei neuen Dateien wird neue ISO mit Suffix erstellt (`-Teil-2`, `-Teil-3`, etc.)
5. **ditto-Staging**:
   - Kopiert Dateien via macOS `ditto` ins temporäre Staging-Verzeichnis
   - Erhält Resource Forks, Extended Attributes und HFS+/APFS-Metadaten
   - Erzeugt ein eigenständiges, vollständiges Archiv
6. **Verzeichnisstruktur**: Unterverzeichnisse werden im ISO übernommen
7. **rclone-Organisation**: ISOs werden in Jahresordner auf Remote-Zielen organisiert
8. **Idempotente Uploads**: rclone läuft auch ohne neue ISOs, um fehlgeschlagene Uploads zu wiederholen

**Beispiel (ab 2.2.0):**
```
Quellverzeichnis/
├── 2026-01-15_Video1.mp4   → Originalmedien-2026-01-15.zip
├── 2026-01-15_Video2.mp4   → Originalmedien-2026-01-15.zip
├── 2026-02-05_Video3.mp4   → Originalmedien-2026-02-05.zip
└── Projekt/
    ├── 2026-01-15_Clip.mp4 → Originalmedien-2026-01-15.zip (Projekt/...)
    └── 2026-02-10_Raw.mov  → Originalmedien-2026-02-10.zip (Projekt/...)
```

**Remote-Struktur:**
```
nas:/Backups/Videos/
└── 2026/
    ├── Originalmedien-2026-01-15.zip
    ├── Originalmedien-2026-01-15.zip.manifest.jsonl   ← Sidecar
    ├── Originalmedien-2026-02-05.zip
    └── Originalmedien-2026-02-10.zip
```

**Vorteile:**
- ✅ Tägliche Organisation für präzise Übersicht
- ✅ Idempotenz vermeidet Duplikate und unnötige I/O
- ✅ Append innerhalb des Cache-Windows: ein ZIP pro Tag, das organisch wächst
- ✅ ZIP_STORED universell lesbar (unzip, 7z, macOS Finder, …)
- ✅ Verzeichnisstruktur bleibt erhalten
- ✅ Sub-Sekunden-Subset-Check via `ZipFile.namelist()` ohne Mount
- ✅ Manifest-Sidecar pro Archiv mit reichhaltigen EXIF/Video-Metadaten

## 📝 Archiv-Format & Technische Details

### Format-Switch ISO → ZIP (ab 2.2.0)

Neue Archive werden als `.zip`-Dateien (`ZIP_STORED`, ohne Kompression)
geschrieben. Vorteile gegenüber dem bisherigen ISO-Format:

- **Streaming-Bau ohne Staging**: `zipfile.ZipFile.write` liest direkt aus
  den Source-Pfaden in das ZIP — kein `ditto`-Staging mehr. Spart pro Tag
  etwa eine Source-Grösse an Disk-Footprint.
- **Sub-Sekunden-Subset-Check**: `ZipFile.namelist()` liefert die komplette
  File-Liste aus dem Central Directory am Ende des ZIPs in Millisekunden —
  kein `hdiutil attach/detach`-Roundtrip mehr.
- **Append-Möglichkeit**: ZIP-Members können streaming-fähig aus einem
  bestehenden ZIP in ein neues kopiert werden (`ZipFile.open('w', force_zip64=True)`
  + `shutil.copyfileobj`). Damit wird "noch ein Clip für denselben Tag" zu
  einem in-place Rebuild des ZIPs, kein `-Teil-N` mehr.
- **Plattform-unabhängig**: kein macOS-`hdiutil` mehr nötig; `zipfile` ist
  Python-Stdlib.

**Determinismus**: Member werden alphabetisch nach `arcname` sortiert geschrieben.
Gleicher Input → byte-identisches ZIP → stabiler SHA-256-Wert im Manifest.

**Bestand bleibt**: ISO-Archive aus 2.0.x bleiben gültig und werden vom
Subset-Check und dem `cache list`/`prune` weiter erkannt. Eine optionale
Bestand-Migration ISO → ZIP folgt in 2.3.0.

### Append-Modell (ab 2.2.0)

Kommen für einen Tag innerhalb des Cache-Windows neue Files dazu, wird das
bestehende ZIP **in-place erweitert**:

1. **Manifest-Pre-Filter** sorgt dafür, dass nur die neuen Files vom NAS
   gezogen werden.
2. **Lazy-Check (Stufe 2)** pullt das `*.zip.manifest.jsonl`-Sidecar vom
   Server (KBs, sub-Sekunde) und prüft per Subset-Vergleich:
   - Jeder (Filename, SHA-256) aus dem Server-Manifest muss im geplanten
     neuen Inhalt vorkommen.
   - Server-Archiv-Grösse muss zum Manifest-Header passen.
   - Bei Konflikt: **Hard-Abort** mit Klartext-Diagnose und Recovery-Hinweis.
3. **In-place Rebuild** des ZIPs (atomar via tempfile + `os.replace`):
   alte Members per Stream-Copy aus alter ZIP, neue Files von Disk dazu.
4. **Manifest neu schreiben**: alte `FileEntry`-Objekte 1:1 übernehmen
   (ZIP_STORED-Bytes sind identisch, SHAs unverändert), neue Files frisch
   via medien-leser-Pipeline erfassen.
5. **Upload mit dynamischem `--immutable`**: für die zu ersetzende ZIP
   wird `--immutable` für diesen einen Upload abgeschaltet; alle anderen
   Archive bleiben beim sicheren Default `--immutable=True`.

### ISO-und-ZIP-Koexistenz

Für Tage mit Bestand-ISO und neuen Files entsteht eine separate `.zip`
**neben** der ISO (kein Append in eine ISO, die ist eingefroren). Beide
sind gültige Archive für denselben Tag; das Manifest-Cache-System ist
format-agnostisch und kennt beide.

### 🔒 Unveränderlichkeit (Immutability)

Drei Schutz-Ebenen:

**Lokal (Arbeitsverzeichnis):**
- Bestehende Archive (`.iso`, `.zip`) werden nie unbeabsichtigt überschrieben.
- Im Append-Fall ist die Modifikation des lokalen ZIPs atomar (tempfile +
  `os.replace`) — eine halbfertige ZIP existiert nie am Zielpfad.

**Replikations-Marker (`.uploaded.json`)**:
- Pro Archiv liegt nach erfolgreichem Upload-zu-allen-Targets ein winziger
  Sidecar mit der Target-Liste + Zeitstempel.
- Voraussetzung für `cache prune`-Safety: nur Archive mit vollständigem
  Marker werden lokal gelöscht.

**Remote (rclone-Ziele):**
- `rclone copyto --immutable` verhindert standardmässig jedes Überschreiben.
- **Dynamische Ausnahme** beim Append: für die ein-bestimmte ZIP, die gerade
  replace-uploadet wird, ist `--immutable` aus. Alle anderen Uploads im
  selben Lauf bleiben weiterhin `--immutable=True`.
- Lazy-Check (Subset-Garantie) vor dem Replace verhindert, dass der
  Server-Inhalt fragmentiert oder partiell überschrieben wird.

### 🔄 Idempotenz: Keine Duplikate

**Archivierungs-Idempotenz:**

Vor dem Bau für einen Tag prüft das Programm:
1. Lokales Archiv (ISO oder ZIP) für diesen Tag vorhanden?
2. Falls ja: format-agnostischer Subset-Check (ZIP-`namelist()` für ZIPs,
   `hdiutil`-Mount für ISOs).
3. Alle Files bereits vorhanden → Tag überspringen.
4. ZIP da, neue Files dabei → **Append-Pfad**: ein einziges ZIP wächst.
5. Nur ISO da, neue Files dabei → eine zusätzliche ZIP entsteht daneben.
6. Kein lokales Archiv, Manifest-Cache deckt den Tag aber ab → Skip
   (Fall C — verhindert Doppel-Upload nach Cache-Prune).

**Upload-Idempotenz:**

Selbst wenn keine neuen Archive erstellt wurden:
1. Existierende Archive aus Arbeitsverzeichnis sammeln.
2. Integritätsprüfung (`ZipFile.testzip()` für ZIP, `hdiutil imageinfo` für ISO).
3. rclone-Synchronisation — identische Files werden als No-Op übersprungen.

## 🤝 Beitragen

Issues und Pull Requests sind willkommen! [GitHub Repository](https://github.com/kurmann/kamera-einleser)

## 📄 Lizenz

MIT License - siehe [LICENSE](LICENSE)

## 👨‍💻 Autor

Patrick Kurmann
