Skip to main content

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 description

🎬 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:CreationDateQuickTime:CreationDateBlackmagic-design:CameraDateRecordedDateTimeOriginalCreateDatefile_birthtimefile_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:002026-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 (brew install exiftool auf macOS)

🚀 Installation

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

🎯 Quick Start

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)

{
  "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)

{
  "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

{
  "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)

{
  "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:

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kurmann_medien_leser-0.2.0.tar.gz (46.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kurmann_medien_leser-0.2.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file kurmann_medien_leser-0.2.0.tar.gz.

File metadata

  • Download URL: kurmann_medien_leser-0.2.0.tar.gz
  • Upload date:
  • Size: 46.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for kurmann_medien_leser-0.2.0.tar.gz
Algorithm Hash digest
SHA256 dd6863bb477d200af37b1fd0c473745552a9dcc9a374ca7142e7abb53a6d9906
MD5 beed839897c7fae2e0794d957b28d548
BLAKE2b-256 57449c639bfcd321a329c7af7a28a8435314551a069484993a72b584ce481c58

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_medien_leser-0.2.0.tar.gz:

Publisher: publish.yml on kurmann/medien-leser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kurmann_medien_leser-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for kurmann_medien_leser-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89957a45b5b9f15771e2e12b59ebadfd030000e74ceb199afe2eb0f1fbb8f573
MD5 abba274ae5698aad3876f15c9b46764c
BLAKE2b-256 5a37aae4abb4e0b0227dca22343fd234d6c4963fcc21744788daa37a90b8805c

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_medien_leser-0.2.0-py3-none-any.whl:

Publisher: publish.yml on kurmann/medien-leser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page