Metadata-Version: 2.4
Name: kurmann-medien-leser
Version: 0.3.0
Summary: Generischer Medien-Metadaten-Leser: exiftool-Wrapper + Best-Guess-Aufnahmedatum mit Provenance. Vereinheitlicht Metadaten aus Blackmagic Camera, iPhone Camera, Final Cut Camera und weiteren Quellen.
Project-URL: Homepage, https://github.com/kurmann/medien-leser
Project-URL: Repository, https://github.com/kurmann/medien-leser
Project-URL: Issues, https://github.com/kurmann/medien-leser/issues
Author: Patrick Kurmann
License-Expression: MIT
Keywords: camera,exif,exiftool,media,metadata,video
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Utilities
Requires-Python: >=3.10
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

# 🎬 medien-leser

Generischer Medien-Metadaten-Leser. Wrappt `exiftool` und liefert ein einheitliches Schema mit **Provenance** — speziell für das Best-Guess-Aufnahmedatum (`recorded_at` + `recorded_at_source`), das über verschiedene Aufnahme-Quellen hinweg konsistent funktioniert.

> **Status: 0.2.0 (Alpha)** — API kann sich in 0.x-Versionen noch ändern. Konsumenten (`kamera-einleser`, `schnittprojekt-leser`) pinnen aktuell auf eine konkrete Version. Ab 1.0.0 ist die API stabil nach SemVer.

## ✨ Was es kann

- **Einheitliche Schemas** für Video-/Foto-Metadaten aus verschiedenen Quellen:
  - `MediaFileMetadata` — Top-Level mit `dates`, `video`, `audio`, `camera`, `gps`, `production`, `source_app`
  - Sub-Schemas: `DateSources`, `VideoInfo`, `AudioInfo`, `CameraInfo`, `GpsInfo`, `ProductionInfo`
- **Best-Guess-Aufnahmedatum** mit dokumentierter Vertrauens-Hierarchie und Provenance-Label:
  - `Keys:CreationDate` → `QuickTime:CreationDate` → `Blackmagic-design:CameraDateRecorded` → `DateTimeOriginal` → `CreateDate` → `file_birthtime` → `file_mtime`
  - Zeitzonen-Inferenz für Apps wie **Final Cut Camera**, die `CreateDate` in UTC ohne TZ-Marker schreiben
  - Format-Normalisierung von `2026:04:20 12:52:15+02:00` → `2026-04-20T12:52:15+02:00` (ISO-8601 mit Doppelpunkt im TZ-Offset)
- **Source-App-Detection**: Heuristik erkennt **Blackmagic Camera**, **iPhone Camera**, **Final Cut Camera** anhand `AppleProappsAppBundleID`, BMD-design-Tags, Apple-Make/Model
- **Batch-Modus** (`extract_metadata_for_files`): N Dateien in einem einzigen `exiftool`-Aufruf → 5-50× schneller als N Einzelaufrufe
- **Verbose-Hooks** (v0.2.0+): konfigurierbarer `exiftool_path` und `on_output`-Callback für rohen exiftool-Output

## 📋 Voraussetzungen

- Python 3.10 oder höher
- [exiftool](https://exiftool.org/) (`brew install exiftool` auf macOS)

## 🚀 Installation

```bash
uv add kurmann-medien-leser
# oder
pipx install kurmann-medien-leser
# oder
pip install kurmann-medien-leser
```

## 🎯 Quick Start

```python
from pathlib import Path
from kurmann_medien_leser import read_metadata, read_dates, pick_recorded_at

# Eine Datei einlesen — volles Schema
metadata = read_metadata(Path("IMG_0001.mov"))

print(metadata.recorded_at)           # "2026-05-03T07:38:55+02:00"
print(metadata.recorded_at_source)    # "exif:CreateDate+tz_from_mtime"
print(metadata.source_app)            # "Final Cut Camera"
print(metadata.camera.model)          # "iPhone 15 Pro Max"
print(metadata.gps.lat, metadata.gps.lon)  # 47.07054, 7.58165
print(metadata.video.codec, metadata.video.resolution)  # "HEVC", "3840x2160"

# Nur Datums-Quellen — schlanke Convenience
dates = read_dates(Path("IMG_0001.mov"))
iso, source = pick_recorded_at(dates)
# → ("2026-05-03T07:38:55+02:00", "exif:CreateDate+tz_from_mtime")

# Custom exiftool-Pfad + Verbose-Modus (v0.2.0+)
metadata = read_metadata(
    Path("clip.mov"),
    exiftool_path="/opt/homebrew/bin/exiftool",
    on_output=lambda line: print(f"[exiftool] {line}", end=""),
)
```

## 🧾 Was kommt zurück? Reale Beispiele

Das `MediaFileMetadata`-Objekt serialisiert sich via `to_dict()` zu folgendem JSON-Format. Felder mit `None`/leeren Werten werden weggelassen — Konsumenten kriegen also typ-spezifisch unterschiedliche Strukturen je nach Quelle.

### iPhone-Camera-Clip (HDR-Video aus dem iPhone)

```json
{
  "recorded_at": "2026-04-20T14:52:15+02:00",
  "recorded_at_source": "exif:Keys:CreationDate",
  "source_app": "iPhone Camera",
  "dates": {
    "keys_creation_date": "2026-04-20T14:52:15+02:00",
    "exif_create_date": "2026-04-20T12:52:15",
    "file_birthtime": "2026-04-20T14:53:02+02:00",
    "file_mtime": "2026-04-20T14:53:02+02:00"
  },
  "video": {
    "codec": "HEVC",
    "compressor_id": "hvc1",
    "resolution": "3840x2160",
    "duration_s": 12.5,
    "fps": 59.997,
    "bitrate_kbps": 87143,
    "bit_depth": 10
  },
  "audio": {
    "codec": "mp4a",
    "channels": 2,
    "sample_rate": 48000
  },
  "camera": {
    "make": "Apple",
    "model": "iPhone 15 Pro Max",
    "software": "18.2",
    "lens": "iPhone 15 Pro Max back triple camera 6.86mm f/1.78"
  },
  "gps": {
    "lat": 47.07054,
    "lon": 7.58165,
    "altitude_m": 522.3
  }
}
```

### Blackmagic-Camera-Clip (BMD-Cam-App auf iOS)

```json
{
  "recorded_at": "2026-05-01T17:22:51+02:00",
  "recorded_at_source": "exif:Keys:CreationDate",
  "source_app": "Blackmagic Camera",
  "dates": {
    "exif_creation_date": "2026-05-01T17:22:51+02:00",
    "exif_create_date": "2026-05-01T15:22:51",
    "blackmagic_date_recorded": "2026-05-01T17:22:51+02:00",
    "file_birthtime": "2026-05-01T17:23:14+02:00",
    "file_mtime": "2026-05-01T17:23:14+02:00"
  },
  "video": { "codec": "HEVC", "resolution": "3840x2160", "fps": 59.993 },
  "audio": { "codec": "mp4a", "channels": 2, "sample_rate": 48000 },
  "camera": {
    "iso": 320,
    "aperture": "f/2.4",
    "shutter_speed": "1/120",
    "white_balance_kelvin": 5600,
    "sensor_fps": 59.993
  },
  "production": {
    "clip_id": "A001_05011722_D031",
    "reel": 1,
    "scene": 5,
    "shot": 31,
    "is_good": true,
    "project": "Familie Sommer 2026",
    "director": "Patrick",
    "day_night": "Day",
    "environment": "Exterior"
  }
}
```

### Final-Cut-Camera-Clip (iOS 18+) — mit TZ-Inferenz

```json
{
  "recorded_at": "2026-05-03T07:38:55+02:00",
  "recorded_at_source": "exif:CreateDate+tz_from_mtime",
  "source_app": "Final Cut Camera",
  "dates": {
    "exif_create_date": "2026-05-03T05:38:55",
    "file_birthtime": "2026-05-03T07:39:00+02:00",
    "file_mtime": "2026-05-03T07:39:00+02:00"
  },
  "video": { "codec": "HEVC", "resolution": "3840x2160" },
  "camera": { "make": "Apple", "model": "iPhone 15 Pro Max" }
}
```

**Beobachte hier**: `exif_create_date` ist Roh-UTC ohne TZ-Marker (`05:38:55`). Final Cut Camera schreibt das so. `pick_recorded_at` erkennt das, leitet die TZ aus `file_mtime` (`+02:00`) ab und konvertiert auf lokale Wallclock (`07:38:55+02:00`). Der `recorded_at_source` dokumentiert das mit dem Suffix `+tz_from_mtime`.

### Klassisches Foto (DSLR oder Smartphone-JPG)

```json
{
  "recorded_at": "2026-04-15T10:24:33",
  "recorded_at_source": "exif:DateTimeOriginal",
  "dates": {
    "exif_datetime_original": "2026-04-15T10:24:33",
    "file_birthtime": "2026-04-15T10:25:01+02:00",
    "file_mtime": "2026-04-15T10:25:01+02:00"
  },
  "camera": {
    "make": "Canon",
    "model": "EOS R5",
    "lens": "RF24-105mm F4 L IS USM"
  }
}
```

**Beobachte hier**: `source_app` ist nicht gesetzt (keine der drei bekannten Apps), die Datei wird dennoch sauber gelesen. `DateTimeOriginal` traditionell ohne TZ-Marker — der Konsument muss wissen, dass das lokale Kamera-Zeit ist (siehe API-Verträge unten).

## 📐 API-Verträge

Was Konsumenten garantiert wissen können:

**Optional-Semantik** — alle Felder ausser `MediaFileMetadata` selbst sind optional:

- `recorded_at = None` und `recorded_at_source = None`, wenn weder EXIF noch Filesystem-Stempel einen Wert liefern (sehr selten — selbst Plain-Text hat eine mtime)
- `gps = None`, wenn keine Geo-Tags vorhanden
- `source_app = None`, wenn keine der drei bekannten Quellen erkannt (Datei wird trotzdem gelesen — Konsument fällt auf generisches EXIF-Verhalten zurück)
- Sub-Schemas (`video`, `audio`, `camera`, `production`) sind immer Objekte, aber ihre Felder sind alle optional. Bei Nicht-Video-Dateien (z.B. JPG) bleibt `video.codec = None` etc.

**Format-Garantien**:

- Datums-Strings sind ISO-8601 (`YYYY-MM-DDTHH:MM:SS[+HH:MM]`) — exiftool-Format wird normalisiert
- `recorded_at` trägt eine TZ, wenn die Quelle eine hat oder TZ inferierbar ist (FCC-Sonderfall)
- `recorded_at` ohne TZ heisst: Quelle war ohne TZ (typisch klassisches EXIF) — Konsument muss interpretieren

**Idempotenz**: `MediaFileMetadata.from_dict(metadata.to_dict()) == metadata` ist nicht garantiert (Defaults werden vom dict-Round-Trip nicht wieder gesetzt), aber `to_dict()` ist deterministisch und semantisch stabil.

**Verhalten bei Nicht-Medien-Dateien**: Wenn exiftool ein leeres dict liefert (z.B. Text, ZIP), gibt `read_metadata` ein `MediaFileMetadata` mit:
- `recorded_at` = `file_mtime` als Fallback (mit Source-Label `"file_mtime"`)
- alle Sub-Schemas leer
- `source_app = None`

→ Konsumenten können also immer `read_metadata` aufrufen ohne vorher den Datei-Typ prüfen zu müssen.

## 📚 Public API

| Symbol | Beschreibung |
|---|---|
| `read_metadata(path, *, exiftool_path="exiftool", on_output=None, raw_metadata=None) → MediaFileMetadata` | Volles Schema. `raw_metadata` für Batch-Optimierung (siehe unten). |
| `read_dates(path, *, exiftool_path="exiftool", on_output=None) → DateSources` | Schlanke Convenience: nur Datums-Quellen. v0.2.0+. |
| `extract_metadata_for_file(path, *, exiftool_path="exiftool", timeout=30.0, on_output=None) → dict` | Roher exiftool-Output (für Debug). |
| `extract_metadata_for_files(paths, *, exiftool_path="exiftool", timeout=300.0, on_output=None) → dict[Path, dict]` | Batch-Modus (5-50× schneller bei N Dateien). |
| `pick_recorded_at(dates) → (iso_string, source_label)` | Best-Guess mit Provenance, anwendbar auf eigene `DateSources`. |
| `detect_source_app(raw_metadata) → str \| None` | Heuristik für Aufnahme-App-Erkennung. |
| `normalize_iso_date(raw) → str \| None` | Format-Normalisierung exiftool → ISO-8601. |
| `check_exiftool_available(exiftool_path="exiftool") → bool` | Smoke-Test, ob die Binary auffindbar ist. |
| `get_exiftool_version(exiftool_path="exiftool") → str` | Version-String der Binary. |
| `MediaFileMetadata`, `DateSources`, `VideoInfo`, `AudioInfo`, `CameraInfo`, `GpsInfo`, `ProductionInfo` | Dataclasses (Type-Hints + `.to_dict()`/`.from_dict()`). |

### Batch-Optimierung

Wenn du N Dateien hast und für jede ein `MediaFileMetadata` willst:

```python
# 1× exiftool-Aufruf für alle Dateien (statt N Einzelaufrufe)
paths = [Path(f) for f in source_files]
batch = extract_metadata_for_files(paths)

# Pro Datei das volle Schema bauen — kein erneuter exiftool-Aufruf
results = {p: read_metadata(p, raw_metadata=batch[p]) for p in paths}
```

Bei 50 Dateien spart das typisch 5-15 Sekunden gegenüber 50 Einzelaufrufen.

## 🎥 Unterstützte Aufnahme-Quellen

Genau wie im Original-Manifest-System aus `kamera-einleser`:

| App | Aufnahmedatum-Quelle | Eigenheiten |
|---|---|---|
| **Blackmagic Camera** | `Keys:CreationDate` mit TZ | viele proprietäre `Blackmagic-design:*`-Felder; Apple ProApps Production (Reel/Scene/Shot/Project/Director); Kamera-Settings (ISO, Shutter, WB, Aperture) |
| **iPhone Camera** | `Keys:CreationDate` mit TZ | Standard Apple QuickTime; Lens-Modell in `VideoKeys:LensModel`; GPS mit Höhe |
| **Final Cut Camera** | nur `CreateDate` (UTC ohne TZ) ⚠️ | identifizierbar via `Keys:AppleProappsAppBundleID = com.apple.FinalCutApp.companion`; TZ wird aus `file_mtime` abgeleitet |

Andere Quellen funktionieren oft auch (Fallback auf klassisches EXIF), kriegen aber `source_app=None`.

## 🤝 Konsumenten im kurmann-Ökosystem

- **[kamera-einleser](https://github.com/kurmann/kamera-einleser)** nutzt `read_metadata` pro Quelldatei beim ISO-Aufbau, plus `extract_metadata_for_files` im Batch-Modus für JSONL-Manifests. Erwartet stabile Schemas — fängt jede neue `source_app`-Konstante ab.
- **[schnittprojekt-leser](https://github.com/kurmann/schnittprojekt-leser)** (geplant ab v0.4.0) nutzt `read_dates` + `pick_recorded_at` für das Aufnahmedatum aus FCPXML-referenzierten Quellclips. Eigene Confidence-Mapping (HIGH/MEDIUM/LOW) auf den `source_label`-Strings.
- **(geplant) schnittprojekt-archivar** wird `extract_metadata_for_files` für Clip-Inventar-JSONL pro archiviertem Schnittprojekt nutzen — kompatibel zum kamera-einleser-Manifest-Format.

## 🛣️ Roadmap

- **0.x**: API-Stabilisierung gemeinsam mit den ersten Konsumenten
- **1.0.0**: stabile API nach SemVer
- **Später**: Audio-/PDF-Metadaten als zusätzliche Schemas (gleicher Stil wie `MediaFileMetadata`)

## 📝 Lizenz

MIT
