Einfacher & solider Medien-Importeur. Archiviert Videoaufnahmen von SSD/iPhone/NAS in ISO-Dateien.
Project description
🎬 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-zipmigrieren. Vollständige Breaking-Changes und Migrations-Pfad: siehe 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
--immutablezu 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. - 🔍 Audit-Befehl:
kamera-einleser audit archive-groupingzeigt 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:
{
"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 (für Cloud-Synchronisation):
brew install rclone - exiftool (für Manifest-Metadaten):
brew install exiftool
🚀 Installation
# 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):
uv tool install git+https://github.com/kurmann/kamera-einleser.git
🎯 Schnellstart
1. Konfigurieren (einmalig)
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:
kamera-einleser config set working_directory /Volumes/Crucial-P3/Originalmedien
kamera-einleser config set working_directory_fallback /Volumes/Samsung2TB/Originalmedien
2. Archivieren
# 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:
# 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.
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
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
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
kamera-einleser manifest show <manifest> # Manifest-Inhalt lesbar anzeigen
kamera-einleser audit archive-grouping <pfad> # Misch-Tag-Archive aus 1.x-Bestand erkennen
settings
kamera-einleser settings # interaktives Konfigurations-Menü
⚙️ Konfigurationsdatei
Die Config liegt XDG-konform unter:
$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
# Interaktive rclone-Konfiguration (NAS, Mega, Drive, ...)
rclone config
# Teste die Verbindung
rclone lsd nas:Originalmedien
In kamera-einleser:
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)
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 mitday_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:
# 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:
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.replaceumbenannt. 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:
- 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. - Stream-Copy: alte Members aus dem bestehenden ZIP werden bytegenau ins neue übernommen (
ZIP_STORED+ Stream-Copy = bit-identisch). - Neue Files kommen frisch dazu.
- Manifest neu schreiben: alte Entries 1:1 reusen, neue Files via medien-leser frisch erfassen.
- Upload mit
--immutable=Falsefü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:
{"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:
- Vor dem Update: ein
cache listlaufen lassen. Wenn.iso-Files auftauchen, vorher auf einer 2.3.x-Installation migrieren:kamera-einleser convert iso-to-zip 2026-04-15 --delete-source-iso
- Update ziehen:
pipx upgrade kurmann-kamera-einleser(Version 3.0.0+). - Config wird beim ersten Start auto-migriert: alte
iso_cache_*-Keys werden inarchive_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.
👨💻 Autor
Patrick Kurmann (@kurmann) — Willisau, Schweiz.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kurmann_kamera_einleser-3.0.2.tar.gz.
File metadata
- Download URL: kurmann_kamera_einleser-3.0.2.tar.gz
- Upload date:
- Size: 81.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6eab0ad1361cce3805f3bc9db99c1fb17802e8e81c49a807910b95b0c0ee1361
|
|
| MD5 |
47e1801cb6070da562888213bba70c99
|
|
| BLAKE2b-256 |
697d57c8f3f772cae702238e26bcd259e04c75a24015c259f8b53c23e675226b
|
Provenance
The following attestation bundles were made for kurmann_kamera_einleser-3.0.2.tar.gz:
Publisher:
publish.yml on kurmann/kamera-einleser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurmann_kamera_einleser-3.0.2.tar.gz -
Subject digest:
6eab0ad1361cce3805f3bc9db99c1fb17802e8e81c49a807910b95b0c0ee1361 - Sigstore transparency entry: 1548753062
- Sigstore integration time:
-
Permalink:
kurmann/kamera-einleser@2a31c6852f74fa06b46783764aed43de8d3d21e6 -
Branch / Tag:
refs/tags/v3.0.2 - Owner: https://github.com/kurmann
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2a31c6852f74fa06b46783764aed43de8d3d21e6 -
Trigger Event:
release
-
Statement type:
File details
Details for the file kurmann_kamera_einleser-3.0.2-py3-none-any.whl.
File metadata
- Download URL: kurmann_kamera_einleser-3.0.2-py3-none-any.whl
- Upload date:
- Size: 97.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acb136c461680758ef9ea66a67bb9a5ba3315a5a094cf0ab273384a3df1b4fea
|
|
| MD5 |
f80abc882fb0ac1362f924051551b11a
|
|
| BLAKE2b-256 |
a0e97a0de14ea70513b552b28ce54ef7f4306b6d0ce0a12127a0b33c6afeecc1
|
Provenance
The following attestation bundles were made for kurmann_kamera_einleser-3.0.2-py3-none-any.whl:
Publisher:
publish.yml on kurmann/kamera-einleser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurmann_kamera_einleser-3.0.2-py3-none-any.whl -
Subject digest:
acb136c461680758ef9ea66a67bb9a5ba3315a5a094cf0ab273384a3df1b4fea - Sigstore transparency entry: 1548753086
- Sigstore integration time:
-
Permalink:
kurmann/kamera-einleser@2a31c6852f74fa06b46783764aed43de8d3d21e6 -
Branch / Tag:
refs/tags/v3.0.2 - Owner: https://github.com/kurmann
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2a31c6852f74fa06b46783764aed43de8d3d21e6 -
Trigger Event:
release
-
Statement type: