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 mitdates,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
CreateDatein 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 einzigenexiftool-Aufruf → 5-50× schneller als N Einzelaufrufe - Verbose-Hooks (v0.2.0+): konfigurierbarer
exiftool_pathundon_output-Callback für rohen exiftool-Output
📋 Voraussetzungen
- Python 3.10 oder höher
- exiftool (
brew install exiftoolauf 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 = Noneundrecorded_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 vorhandensource_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) bleibtvideo.codec = Noneetc.
Format-Garantien:
- Datums-Strings sind ISO-8601 (
YYYY-MM-DDTHH:MM:SS[+HH:MM]) — exiftool-Format wird normalisiert recorded_atträgt eine TZ, wenn die Quelle eine hat oder TZ inferierbar ist (FCC-Sonderfall)recorded_atohne 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_mtimeals 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_metadatapro Quelldatei beim ISO-Aufbau, plusextract_metadata_for_filesim Batch-Modus für JSONL-Manifests. Erwartet stabile Schemas — fängt jede neuesource_app-Konstante ab. - schnittprojekt-leser (geplant ab v0.4.0) nutzt
read_dates+pick_recorded_atfür das Aufnahmedatum aus FCPXML-referenzierten Quellclips. Eigene Confidence-Mapping (HIGH/MEDIUM/LOW) auf densource_label-Strings. - (geplant) schnittprojekt-archivar wird
extract_metadata_for_filesfü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
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_medien_leser-0.3.0.tar.gz.
File metadata
- Download URL: kurmann_medien_leser-0.3.0.tar.gz
- Upload date:
- Size: 50.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8fbbf53c751d1101706a49c61816b5713b6d36fa894bb5964de1ba7c81ce82e
|
|
| MD5 |
f3ef04e2b6e56c8ca2d6fcd9400090c3
|
|
| BLAKE2b-256 |
50944ef96b864bb997c98ca255e8e9f20f28982fdc3f4abce945b477775bd899
|
Provenance
The following attestation bundles were made for kurmann_medien_leser-0.3.0.tar.gz:
Publisher:
publish.yml on kurmann/medien-leser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurmann_medien_leser-0.3.0.tar.gz -
Subject digest:
e8fbbf53c751d1101706a49c61816b5713b6d36fa894bb5964de1ba7c81ce82e - Sigstore transparency entry: 1541525067
- Sigstore integration time:
-
Permalink:
kurmann/medien-leser@b13147995257f9d9c07d0b5eed34c6af9cc3b1fc -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kurmann
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b13147995257f9d9c07d0b5eed34c6af9cc3b1fc -
Trigger Event:
release
-
Statement type:
File details
Details for the file kurmann_medien_leser-0.3.0-py3-none-any.whl.
File metadata
- Download URL: kurmann_medien_leser-0.3.0-py3-none-any.whl
- Upload date:
- Size: 21.2 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 |
fada04428635ff40389a752c8d803da3805f2187554243d2a608efd5d6301a93
|
|
| MD5 |
4b4ec26ea72762c105e5c5d502f28c1f
|
|
| BLAKE2b-256 |
9d91c1176a0114076c531eba85cef748faed61a85aa7814306b12c7a7283a1b0
|
Provenance
The following attestation bundles were made for kurmann_medien_leser-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on kurmann/medien-leser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurmann_medien_leser-0.3.0-py3-none-any.whl -
Subject digest:
fada04428635ff40389a752c8d803da3805f2187554243d2a608efd5d6301a93 - Sigstore transparency entry: 1541525161
- Sigstore integration time:
-
Permalink:
kurmann/medien-leser@b13147995257f9d9c07d0b5eed34c6af9cc3b1fc -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kurmann
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b13147995257f9d9c07d0b5eed34c6af9cc3b1fc -
Trigger Event:
release
-
Statement type: