Metadata-Version: 2.4
Name: kurmann-kamera-einleser
Version: 3.0.1
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<1.0.0,>=0.3.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/NAS in **ZIP-Dateien pro Aufnahmetag** (`ZIP_STORED`, ohne Kompression) und synchronisiert sie auf beliebige rclone-Ziele (Synology NAS, Mega.nz, etc.).

> **3.0.0 ist ein Aufräum-Major-Release.** Der ISO-Code-Pfad ist
> komplett entfernt. Das Tool ist ab 3.0.0 ein reines ZIP-Werkzeug
> ohne Legacy-Ballast. Bestand-ISOs aus 1.x/2.0.x lassen sich vorher
> auf 2.3.x mit `convert iso-to-zip` migrieren. Vollständige
> Breaking-Changes und Migrations-Pfad: siehe
> [CHANGELOG.md](CHANGELOG.md).

## ✨ Features

- **🗜 ZIP-Archive** (`ZIP_STORED`, ohne Kompression). Streaming-Bau direkt aus den Source-Pfaden, kein Staging-Zwischenspeicher.
- **📦 Append in bestehende ZIPs** im Cache-Window (Default 7 Tage). Neue Files vom selben Tag werden in das bestehende ZIP eingefügt — kein `-Teil-N`, ein ZIP pro Tag.
- **🛡️ Lazy-Check vor Append**: Vor jedem Server-überschreibenden Upload wird das Server-Manifest gepullt (sub-Sekunde, KBs) und gegen den geplanten Inhalt verifiziert. Würde eine Datei verloren gehen, blockt der Lauf hart.
- **📅 Tag-Gruppierung nach echtem Aufnahmedatum** aus EXIF (statt Filesystem-`mtime`). Misch-Tag-Archive aus 1.x sind strukturell gelöst.
- **🔁 Idempotente Archivierung**: ein Re-Run mit denselben Sources tut nichts — Pre-Filter aus den Manifest-Sidecars erkennt jede einzelne archivierte Datei in Millisekunden.
- **☁️ rclone-Multi-Target**: parallel zu NAS und Cloud mit dynamischem `--immutable`-Schutz (Append-Replace wird gezielt ausgenommen).
- **♻️ Auto-Prune lokal** (Cache-Variante C): ZIPs älter als 7 Tage werden lokal entfernt — aber nur, wenn der `*.uploaded.json`-Marker bestätigt, dass das Archiv auf **allen** Targets liegt.
- **📥 Auto-Fetch**: kommt ein Tag aus dem Cache-Window ins Archive-Flow, wird sein ZIP automatisch vom ersten Target gezogen, damit der Append-Pfad greift statt am `--immutable` zu scheitern.
- **🚦 Startup-Guard**: leerer Manifest-Cache + Server hat Archive → harter Abbruch mit `cache sync-manifests`-Hinweis. Schützt vor dem Zweit-Rechner-Worst-Case (stundenlanger Pull + Wall-of-immutable-Errors).
- **📑 Reichhaltige Manifests pro Archiv** (JSONL-Sidecar): pro Datei 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).
- **🔍 Audit-Befehl**: `kamera-einleser audit archive-grouping` zeigt Misch-Tag-Archive aus dem Bestand.
- **🔧 XDG-konforme TOML-Config** mit `kamera-einleser config show/get/set`.

## 🎬 Unterstützte Aufnahme-Quellen

**kamera-einleser vereinheitlicht die Metadaten aus verschiedenen iPhone-Kamera-Apps in ein einziges, konsistentes Manifest-Schema.** Jede App-Familie schreibt EXIF-Tags in eigene Atome — der Indexer kennt die Eigenheiten und produziert für alle dasselbe Schema, mit klarer Provenance über die Quelle und das Aufnahmedatum.

| App | Aufnahmedatum | Reiche Metadaten |
|---|---|---|
| **Blackmagic Camera** | `Keys:CreationDate` mit TZ ✓ | Apple ProApps Production-Felder (Reel/Scene/Shot/Project/Director), Kamera-Settings (ISO, Shutter, WB, Aperture) |
| **iPhone Camera** (Apple Foto-App) | `Keys:CreationDate` mit TZ ✓ | Lens-Modell, GPS mit Höhe |
| **Final Cut Camera** (Apple, iOS 18+) | `CreateDate` UTC ohne TZ ⚠️ — Indexer leitet TZ aus `file_mtime` ab | Lens-Modell, GPS mit Höhe, App-Version |

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

```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", "lens": "..."},
  "gps": {"lat": 47.07054, "lon": 7.58165, "altitude_m": 516.0},
  "video": {"codec": "HEVC", "resolution": "3840x2160", "fps": 30}
}
```

Andere Apps funktionieren oft auch — kriegen `source_app=null` und können einzelne Felder vermissen. `recorded_at_source` zeigt immer, welche EXIF-Quelle gewählt wurde, damit Konsumenten dem Wert vertrauen oder gezielt nachprüfen können.

## 📋 Voraussetzungen

- macOS (Working-Dir-Pfade gehen von APFS aus; Hot-Path ist plattformunabhängig)
- Python 3.10 oder höher
- [rclone](https://rclone.org/) (für Cloud-Synchronisation): `brew install rclone`
- [exiftool](https://exiftool.org/) (für Manifest-Metadaten): `brew install exiftool`

## 🚀 Installation

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

# Alternativ: in eine virtualenv via pip
pip install kurmann-kamera-einleser
```

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

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

## 🎯 Schnellstart

### 1. Konfigurieren (einmalig)

```bash
kamera-einleser settings
```

Interaktives Menü für:

- **Quellverzeichnisse**: lokal (`/Volumes/Crucial-P3/Eingang`) oder rclone-Pfad (`synology:/volume1/Fotos/Eingang`)
- **rclone-Targets**: erstes ist konventionell das lokale NAS, weiteres die Cloud
- **Arbeitsverzeichnis**: lokaler Cache für ZIPs (primär + optionaler Fallback auf zweiter SSD)
- **Ausschluss-Patterns**: glob-Muster, die ignoriert werden (Default: `._*`, `.DS_Store`)

Oder über die CLI:

```bash
kamera-einleser config set working_directory /Volumes/Crucial-P3/Originalmedien
kamera-einleser config set working_directory_fallback /Volumes/Samsung2TB/Originalmedien
```

### 2. Archivieren

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

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

### 3. Zweit-Rechner einrichten

Beim ersten Lauf auf einer frischen Maschine, die das gleiche NAS nutzt:

```bash
# Konfiguration wie oben
kamera-einleser settings

# Manifest-Cache vom NAS aufbauen (KBs pro Archiv, sub-Sekunde)
kamera-einleser cache sync-manifests

# Jetzt ist der Pre-Filter scharf — Re-Archivierungen werden lautlos übersprungen
kamera-einleser archive
```

Ohne Schritt 2 würde der Startup-Guard mit klarem Recovery-Hinweis abbrechen.

## 📖 Befehle

### `archive`

Der Hauptbefehl. Liest alle konfigurierten Quellen, gruppiert nach Aufnahmedatum, baut ZIPs, lädt zu allen Targets hoch.

```bash
kamera-einleser archive [SOURCE_PATH] [--delete-source] [--no-upload]
                        [--prune-cache/--no-prune-cache]
                        [--auto-fetch/--no-auto-fetch] [--force]
```

- `--delete-source`: nach erfolgreichem Upload Originaldateien löschen (fragt nach)
- `--no-upload`: nur lokal bauen, nicht zu Targets pushen
- `--prune-cache/--no-prune-cache`: Auto-Prune am Lauf-Ende übersteuern
- `--auto-fetch/--no-auto-fetch`: Auto-Fetch für out-of-window-Tage übersteuern
- `--force`: Startup-Guard übergehen (nach bewusster Remote-Löschung)

### `config`

```bash
kamera-einleser config              # aktuelle Konfiguration anzeigen
kamera-einleser config show         # idem
kamera-einleser config path         # Pfad der TOML-Datei
kamera-einleser config list         # alle Werte als TOML
kamera-einleser config get <key>    # einzelner Wert
kamera-einleser config set <key> <value>
```

### `cache`

```bash
kamera-einleser cache list                                # lokale ZIPs mit Status
kamera-einleser cache fetch <day-or-archive>              # einzelnes Archiv vom NAS holen
kamera-einleser cache sync-manifests                      # alle Manifest-Sidecars vom NAS
kamera-einleser cache prune [--max-size-gb] [--max-days] [--dry-run]
kamera-einleser cache resync                              # nicht-replizierte ZIPs nachschieben
```

### `manifest` / `audit`

```bash
kamera-einleser manifest show <manifest>             # Manifest-Inhalt lesbar anzeigen
kamera-einleser audit archive-grouping <pfad>        # Misch-Tag-Archive aus 1.x-Bestand erkennen
```

### `settings`

```bash
kamera-einleser settings   # interaktives Konfigurations-Menü
```

## ⚙️ Konfigurationsdatei

Die Config liegt XDG-konform unter:

```text
$XDG_CONFIG_HOME/kamera-einleser/config.toml
# meistens: ~/.config/kamera-einleser/config.toml
```

Vollständige Schlüssel:

| Schlüssel | Typ | Default | Bedeutung |
|---|---|---|---|
| `source_directories` | list | `[]` | Quellen (lokale Pfade oder rclone-Specs `remote:path`) |
| `rclone_targets` | list of dicts | `[]` | `[{name, path}, ...]` |
| `working_directory` | string | `""` | Primärer Cache für ZIPs |
| `working_directory_fallback` | string | `""` | Sekundär (z.B. externe SSD), wenn primär fehlt/voll |
| `archive_cache_max_bytes` | int | `500_000_000_000` | Cache-Limit in Byte (500 GB), `0` = aus |
| `archive_cache_max_days` | int | `7` | Cache-Window in Tagen (Variante C), `0` = aus |
| `archive_auto_fetch` | bool | `true` | Out-of-window-Tag automatisch vom NAS holen |
| `excludes` | list | `["._*", ".DS_Store"]` | Glob-Patterns für Source-Scan |

## 🔧 rclone einrichten

```bash
# Interaktive rclone-Konfiguration (NAS, Mega, Drive, ...)
rclone config

# Teste die Verbindung
rclone lsd nas:Originalmedien
```

In `kamera-einleser`:

```bash
kamera-einleser settings
# → rclone-Ziele verwalten → Hinzufügen
#   Name: nas
#   Pfad: nas:Originalmedien
```

Konvention: das **erste** Target ist das lokale/schnelle (NAS), weitere sind Cold-Storage. Beim Lazy-Check und beim Manifest-Pull wird immer das erste Target gefragt.

## 🎯 Archivierungs-Konzept

### Working-Directories (primär + Fallback)

```bash
kamera-einleser config set working_directory /Volumes/Crucial-P3/Originalmedien
# Primär: schnelle interne/externe SSD

kamera-einleser config set working_directory_fallback /Volumes/Samsung2TB/Originalmedien
# Fallback: grössere SSD für 100GB+-Pulls vom NAS
```

**Split-Pipeline**: bei knappem Platz auf dem Primary werden Pull (vom NAS) und ZIP-Bau automatisch auf zwei Volumes aufgeteilt — Pull aufs Primary, ZIP aufs Fallback. Jede Disk braucht nur ~1.5× Quell-Grösse statt ~3.5× zusammen.

### Lokaler Archiv-Cache (Variante C)

Die ZIPs bleiben nach erfolgreichem Upload im Working-Dir liegen, damit Folge-Läufe schnell sind (Append in das gleiche ZIP ohne erneuten Server-Roundtrip). Aufräumen passiert automatisch:

- **Tag-basiert** (`archive_cache_max_days`, Default 7): ZIPs mit `day_key` älter als heute−7 werden geprunt — aber nur, wenn ihr `*.uploaded.json`-Marker bestätigt, dass sie auf **allen** Targets liegen.
- **Bytes-Backstop** (`archive_cache_max_bytes`, Default 500 GB): zusätzliche Obergrenze. Wird sie nach dem Tag-Prune noch überschritten, fliegen weitere alte ZIPs raus.

Pro Lauf werden beide Limits geprüft. Operator-Eingriffe:

```bash
# Limits anpassen
kamera-einleser config set archive_cache_max_days 14
kamera-einleser config set archive_cache_max_bytes 1000000000000  # 1 TB

# Manuell aufräumen
kamera-einleser cache list
kamera-einleser cache prune --max-days 14 --dry-run
kamera-einleser cache prune --max-days 14

# Einen einzelnen Lauf override
kamera-einleser archive --no-prune-cache
```

Der lokale Cache ist **kein** Backup mit Garantien — die echten Sicherungen sind die rclone-Targets. Der Cache existiert nur für schnellen Append und für Wiederholung bei Teil-Uploads.

### Multi-Source-Verarbeitung

Sind mehrere Quellverzeichnisse konfiguriert, werden sie sequenziell verarbeitet:

```bash
kamera-einleser archive
# → 1. /Volumes/Crucial-P3/Eingang
# → 2. synology:/volume1/Fotos/Eingang
# → 3. /Volumes/Sandisk/Eingang
```

Pro Quelle wird unabhängig idempotent geprüft. Eine fehlgeschlagene Quelle blockiert die anderen nicht.

## 📦 Archiv-Format & technische Details

### ZIP-Bau (`ZIP_STORED`)

- **Ohne Kompression** — Video-Codecs (HEVC, ProRes etc.) komprimieren bereits intern; eine zweite Schicht würde nur CPU verbrennen ohne Platz zu sparen.
- **Streaming**: jede Source-Datei wird direkt aus dem Filesystem in den ZIP-Stream geschrieben, kein Staging-Zwischenspeicher (spart pro Tag ≈ 1× Source-Grösse Disk-Footprint).
- **Atomar**: ZIP wird unter Temp-Name gebaut und via `os.replace` umbenannt. Bei Abbruch entsteht nie ein halbes Archiv.
- **Members sortiert** (alphabetisch nach Pfad) — deterministische SHA-256.

### Append-Modell

Für ZIPs innerhalb des Cache-Window (Default 7 Tage) wird beim Re-Lauf mit neuen Files für denselben Tag das bestehende ZIP **in-place erweitert**. Mechanik:

1. **Lazy-Check Stufe 2**: das `.zip.manifest.jsonl`-Sidecar vom ersten Target wird gepullt (KBs, sub-Sekunde) und gegen den geplanten neuen Inhalt verifiziert. Subset-Garantie: jeder `(filename, sha256)` aus dem Server-Manifest muss im neuen Plan vorkommen.
2. **Stream-Copy**: alte Members aus dem bestehenden ZIP werden bytegenau ins neue übernommen (`ZIP_STORED` + Stream-Copy = bit-identisch).
3. **Neue Files** kommen frisch dazu.
4. **Manifest neu schreiben**: alte Entries 1:1 reusen, neue Files via medien-leser frisch erfassen.
5. **Upload mit `--immutable=False`** für genau dieses Archiv — alle anderen Uploads bleiben beim sicheren Default.

Bei Verletzung der Subset-Garantie wird **hart abgebrochen** mit Klartext-Diagnose (welche Files würden verloren gehen). Manueller Eingriff via `cache fetch <day>` + erneuter Lauf.

### Manifest-Sidecars

Jedes ZIP trägt automatisch ein `*.zip.manifest.jsonl` daneben mit:

- **Header**: ZIP-SHA-256, Grösse, File-Count, Tool-Version, Timestamp
- **Pro-File-Entries**: relative_path, size, sha256, MediaFileMetadata (recorded_at + Provenance, camera, gps, video/audio-Codec, Apple-ProApps-Production-Felder)

Das Manifest wandert beim Upload mit zum NAS — kein `--immutable` für Manifests (Re-Indexierungen sind valide Updates).

### Replikations-Marker (`*.uploaded.json`)

Nach erfolgreichem Upload zu allen Targets wird ein `*.uploaded.json` neben dem lokalen ZIP geschrieben:

```json
{"targets": ["nas", "mega"], "uploaded_at": "2026-05-14T09:01:23+02:00", "tool_version": "3.0.0"}
```

Maschinenlokal (nicht auf Server). Cache-Prune respektiert diesen Marker: gelöscht wird nur, wenn die Target-Liste die aktuell konfigurierten Targets vollständig abdeckt.

## 🛠 Migration von 2.x

Wer von 2.0.x–2.3.x kommt:

1. **Vor dem Update**: ein `cache list` laufen lassen. Wenn `.iso`-Files auftauchen, vorher auf einer 2.3.x-Installation migrieren:
   ```bash
   kamera-einleser convert iso-to-zip 2026-04-15 --delete-source-iso
   ```
2. **Update ziehen**: `pipx upgrade kurmann-kamera-einleser` (Version 3.0.0+).
3. **Config wird beim ersten Start auto-migriert**: alte `iso_cache_*`-Keys werden in `archive_cache_*` umgesetzt; ist transparent.

Wer das Update zieht, bevor alle ISOs migriert sind, hat **keinen Datenverlust** — ISO-Files bleiben physisch liegen, werden vom Tool aber ignoriert. Downgrade auf 2.3.1 für Konvertierung ist jederzeit möglich.

Wer von 1.x kommt, hat einen längeren Migrations-Pfad — am einfachsten zuerst auf 2.3.1 upgraden, mtime-gruppierte ISOs mit `convert iso-to-zip` neu nach `recorded_at` umpacken (Multi-Day-Split heilt sich automatisch), dann auf 3.0.0.

## 🤝 Beitragen

Pull-Requests willkommen. Architektur folgt dem `kurmann-python-api`-Skill: Schichten `core/api/cli/services/models`, Request/Result-Signaturen, dateibasierte Outputs.

## 📄 Lizenz

MIT — siehe [LICENSE](LICENSE).

## 👨‍💻 Autor

Patrick Kurmann ([@kurmann](https://github.com/kurmann)) — Willisau, Schweiz.
