Skip to main content

Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten (FCPXML, iMovieMobile).

Project description

kurmann-schnittprojekt-leser

Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten. Liest FCPXML (Final Cut Pro und LumaFusion) sowie iMovieMobile-ZIP-Bundles (iOS-iMovie) und beantwortet strukturierte Fragen zum Projekt – primär: Aufnahmedatum des ersten Clips für die automatische Datei-Naming- und Text-Overlay-Vorbereitung im familienfilm-manager.

Designprinzip: Der Leser schreibt niemals in Schnittprojekte. Er ist ausschliesslich Beobachter.

Voraussetzungen

  • Python ≥ 3.11 (nutzt StrEnum, tomllib, zoneinfo aus der Standardlib).
  • exiftool – Pflicht für FCPXML-Projekte. Liest Aufnahmedaten aus den referenzierten Quelldateien (MOV/MP4/HEIC/JPG) via exiftool-Subprozess.
    • macOS: brew install exiftool
    • Linux: sudo apt install libimage-exiftool-perl
    • Für iMovieMobile-Projekte nicht nötig – das Datum lebt direkt im iMovieMobileProject.plist.
  • rclone – optional. Nur nötig, wenn RuntimeOptions.source_search_roots rclone-Remotes enthält (z. B. lyssach-nas:/Videoschnitt/...). Auch nur für FCPXML-Pfad relevant.

Installation

uv pip install kurmann-schnittprojekt-leser
# oder klassisch
pip install kurmann-schnittprojekt-leser

Lokale Entwicklung (Repo geklont):

uv venv
uv pip install -e ".[dev]"
uv run pytest

Verwendung

CLI

Ein Subcommand unter inspect (seit v0.5.0):

schnittprojekt-leser inspect summary <pfad> [--format text|json] [--include-clips] [--timezone TZ]

Beispiel: Projekt-Übersicht (Text):

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxmld" --timezone Europe/Zurich
Projekt: Mein Schnitt
Format: fcpxml_bundle (Schema 1.8)
Auflösung: 3840×2160
Farbraum: 9-16-9 (Rec. 2020 PQ)
Audio: stereo
Timeline-Dauer: 164.1s
Clips primär: 4
Clips total (inkl. sekundärer Spuren): 5
Projektdatei mtime: 2026-04-06T21:32:08+02:00

Aufnahme:
  Datum: 2026-03-29
  Zeitstempel: 2026-03-29T08:20:02+02:00
  Quelle: exif:Keys:CreationDate
  Konfidenz: high

Beispiel: Vollständiges Result als JSON (für familienfilm-manager):

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxmld" \
    --timezone Europe/Zurich --format json
{
  "success": true,
  "project_name": "Mein Schnitt",
  "project_format": "fcpxml_bundle",
  "schema_version": "1.8",
  "timeline_duration_seconds": 164.1,
  "clip_count_primary": 4,
  "clip_count_total": 5,
  "video_resolution": [3840, 2160],
  "color_space": "9-16-9 (Rec. 2020 PQ)",
  "audio_layout": "stereo",
  "project_mtime_iso": "2026-04-06T21:32:08+02:00",
  "recording": {
    "first_clip_date": "2026-03-29",
    "first_clip_datetime": "2026-03-29T08:20:02+02:00",
    "first_clip_source_label": "exif:Keys:CreationDate",
    "first_clip_confidence": "high",
    "first_clip_resolved_source_file": "/.../A001_03290820_D498.mov"
  },
  "clips": null,
  "warnings": []
}

Skript-Pipelines für nur das Datum: jq auf den recording-Block.

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxmld" --format json \
    | jq -r '.recording.first_clip_date'
2026-03-29

Beispiel: Clip-Inventar einbeziehen:

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxmld" --include-clips --format json \
    | jq '.clips | length, .clips[0]'
4
{
  "spine_index": 0,
  "asset_id": "a1",
  "clip_name": "A001_03290820_D498",
  "relative_path": "A001_03290820_D498.mov",
  "is_video": true,
  "resolution_status": "resolved",
  "resolved_uri": "/.../A001_03290820_D498.mov",
  "media_metadata": {
    "recorded_at": "2026-03-29T08:20:02+02:00",
    "recorded_at_source": "exif:Keys:CreationDate",
    "source_app": "iPhone Camera",
    "video": { "codec": "HEVC", "resolution": "3840x2160" },
    "camera": { "make": "Apple", "model": "iPhone 15 Pro Max" }
  }
}

Siehe Sektion Scope von include_clips weiter unten — heute nur Video-Originalmedien.

Beispiel: Quelldateien auf einem rclone-Remote suchen lassen:

schnittprojekt-leser inspect summary "Mein Schnitt.fcpxml" \
    --source-root "lyssach-nas:/Videoschnitt/Luma Fusion-Export" \
    --source-depth 2 \
    --timezone Europe/Zurich

stdout/stderr-Disziplin:

  • stdout – nur Ergebnisdaten (Datum, JSON, Übersicht). Pipeline-fähig.
  • stderr – Events, Warnungen, Fehlermeldung, Diagnose; rohen exiftool/rclone-Output nur mit --verbose.
  • --verbose ändert nie stdout.

Exit-Codes: 0 Erfolg, 1 Laufzeitfehler, 2 Argument-/Config-Fehler.

Als Bibliothek

from pathlib import Path

from schnittprojekt_leser.api import read_project_summary
from schnittprojekt_leser.models import (
    ReadProjectSummaryRequest,
    RuntimeOptions,
    RcloneSource,
)

# 1. Projekt-Übersicht inkl. Recording-Info (Default: ohne Clip-Inventar)
result = read_project_summary(
    ReadProjectSummaryRequest(
        project_path=Path("Mein Schnitt.fcpxmld"),
        timezone_assumption="Europe/Zurich",
    ),
    RuntimeOptions(
        source_search_roots=[
            RcloneSource(remote="lyssach-nas", path="/Videoschnitt/Luma Fusion-Export"),
            Path("/Volumes/Aufnahme-Archiv"),
        ],
        source_search_max_depth=2,
    ),
)
if result.success:
    print(result.project_name, result.video_resolution)
    print(result.recording.first_clip_date, result.recording.first_clip_confidence)
else:
    print("Fehler:", result.error_message)

# 2. Mit vollem Clip-Inventar (für Archivar-/Indexierungs-Use-Cases)
result = read_project_summary(
    ReadProjectSummaryRequest(
        project_path=Path("Mein Schnitt.fcpxmld"),
        include_clips=True,
    ),
    RuntimeOptions(),
)
for clip in result.clips or []:
    if clip.media_metadata:
        print(clip.relative_path, clip.media_metadata.recorded_at,
              clip.media_metadata.source_app)

Konfiguration

RuntimeOptions enthält ausschliesslich technische Parameter (Werkzeug-Pfade, Timeouts, Suchverzeichnisse). Fachliche Parameter wie project_path oder timezone_assumption gehören in den jeweiligen Request.

Feld Default Bedeutung
exiftool_path "exiftool" Pfad zum exiftool-Binary (PATH-Auflösung).
exiftool_timeout_seconds 30.0 Timeout pro exiftool-Aufruf.
rclone_path "rclone" Pfad zum rclone-Binary (PATH-Auflösung). Nur für rclone-Quellen.
rclone_timeout_seconds 60.0 Timeout pro rclone-Aufruf.
source_search_roots [] Zusätzliche Suchverzeichnisse für Quelldateien (Path lokal oder RcloneSource).
source_search_max_depth 2 Rekursionstiefe pro Search-Root. 0 = nur Wurzel, 1 = +eine Ebene, etc.
temp_dir None Temp-Verzeichnis für rclone-Downloads. None = System-Temp.

Datenebenen-Modell

Jede Antwort sitzt auf genau einer dieser vier Ebenen oder ist eine bewusste Komposition daraus:

  1. Projekt – Name, Schema-Version, Auflösung, Audio-Layout, Timeline-Dauer, Anzahl Clips, Projektdatei-mtime.
  2. Timeline – geordnete Spine-Clip-Sequenz, Lane-Sub-Spines.
  3. Asset (Resource) – Eintrag aus dem <resources>-Pool: src-Verweis, Format, Audio-Charakteristik.
  4. Quelldatei – die tatsächliche .mov/.heic/.jpg. Hier wohnen QuickTime/EXIF-Metadaten, GPS, Filesystem-mtime/birthtime.

read_project_summary operiert standardmässig auf Projekt + Timeline + ein Asset-Lookup für die Recording-Info (Komposition Timeline → Asset → Quelldatei für den ersten Clip). Mit include_clips=True zusätzlich der volle Pass durch alle Top-Level-Asset-Clips (siehe Scope von include_clips unten).

Hierarchie der Datums-Quellen

Die Datums-Quellen unterscheiden sich pro Format. Bei iMovieMobile lebt das Aufnahmedatum direkt im Projekt-Plist (editList[i].creationDate) – ein Quelldatei-Lesepass ist nicht nötig. Bei FCPXML muss die Quelldatei mit exiftool gelesen werden, weil weder Final Cut Pro noch LumaFusion das Aufnahmedatum im XML mitspeichern.

iMovieMobile

Quelle Source-Label Konfidenz
editInfo.editList[i].creationDate (erster Clip mit clipType=1) imovie:editList:creationDate HIGH

Bei Abweichung zwischen editList[i].creationDate und editList[i].movie.creationDate gewinnt der Clip-Wert; die Differenz steht als Warnung im Result.

FCPXML

Spiegelt das Vorbild aus kamera-einleser (siehe Hinweis zur Code-Spiegelung am Ende). Erste Quelle mit nicht-leerem Wert gewinnt:

Position Quelle Source-Label Konfidenz
1 Keys:CreationDate (Apple Item-List, mit TZ) exif:Keys:CreationDate HIGH
2 QuickTime:CreationDate (UDTA, mit TZ) exif:CreationDate HIGH
3 Blackmagic-design:CameraDateRecorded exif:Blackmagic-design:CameraDateRecorded HIGH
4 DateTimeOriginal (klassisches EXIF) exif:DateTimeOriginal HIGH
5 CreateDate (QuickTime MVHD, oft UTC ohne TZ) exif:CreateDate (ggf. +tz_from_mtime) MEDIUM
6 Filesystem st_birthtime (macOS APFS/HFS+) file_birthtime MEDIUM
7 Filesystem st_mtime file_mtime LOW
Notnagel Projektdatei-mtime, wenn Quelldatei nicht auflösbar project_fallback LOW

Konflikt-Strategie: die hierarchisch höchste Quelle gewinnt. Abweichende Sekundär-Werte werden als warnings mitgegeben. confidence und source_label machen jederzeit nachvollziehbar, woher der Wert kam.

Final-Cut-Camera-Sonderfall

Apples Final Cut Camera (iOS 18+) schreibt CreateDate in UTC ohne TZ-Marker. Wird der Wert als-ist angezeigt, missverstehen Konsumenten ihn als lokale Zeit (1–2 Stunden falsch). Die Bibliothek erkennt diesen Fall: wenn exif_create_date ohne TZ-Marker ist und file_mtime eine TZ trägt, wird die TZ inferiert und auf den UTC-Wert angewendet. Das Source-Label bekommt das Suffix +tz_from_mtime – Konsumenten sehen sofort, dass die TZ abgeleitet ist.

Timezone-Verhalten

request.timezone_assumption ist die intendierte lokale Zeitzone des Konsumenten (IANA-Name, z. B. "Europe/Zurich"). Sie wirkt konsistent auf beide Felder iso_datetime und iso_date: ist sie gesetzt, werden Stempel in diese Zone konvertiert; ist sie None, bleiben Stempel in ihrer Eigen-TZ (bzw. UTC bei naiven Werten).

Stempel Assumption iso_datetime iso_date
Aware (z. B. …+02:00) gesetzt konvertiert in Assumption-TZ Tag in Assumption-TZ
Aware None unverändert (Eigen-TZ des Stempels) Tag in Eigen-TZ des Stempels
Naive (kein Offset) gesetzt als Assumption-Lokalzeit interpretiert Tag in Assumption-TZ
Naive None als UTC interpretiert UTC-Tag

Für Familienfilme zählt der Wallclock-Tag. Beispiel iMovie-Plist: 2026-04-23T15:52:21Z (UTC) wird mit --timezone Europe/Zurich zu 2026-04-23T17:52:21+02:00 (CEST) – passt zum macOS-Finder, der die gleiche Wallclock-Zeit anzeigt. Ohne Assumption führt UTC-Z-Stamps in der Schweiz (CEST) bei späten Abend-Aufnahmen zu Datumsverschiebungen um einen Tag. Für familienfilm-manager gilt: immer timezone_assumption="Europe/Zurich" setzen.

„Erster Clip" – formatkonkret

  • FCPXML: erster <asset-clip> direkt unter <spine> (nicht in lane-Sub-Spines), der ein Asset mit is_real_source=True referenziert. Generator/Title-Refs (z. B. *.titleData) werden übersprungen.
  • Edge Case: leere Spine oder nur Generators → success=False, Fehlermeldung „Schnittprojekt enthält keinen primären Video-Clip mit Quelldatei".
  • Edge Case: erste echte Quelldatei nur auf lane > 0 (sekundäre Spur) → Phase 1 sucht nicht dort. Konvention: Familienfilm-Schnitte folgen der Spine-Konvention; Sonderfälle sollen auffällig fehlschlagen statt still zu raten.

rclone-Quellen und Umlaute

Quellverzeichnisse können lokal oder rclone-Remotes sein:

from schnittprojekt_leser.models import RcloneSource, parse_source_spec
from pathlib import Path

# Direkt:
RcloneSource(remote="lyssach-nas", path="/Videoschnitt/Luma Fusion-Export")

# Aus Config-String (gleiches Format wie kamera-einleser):
parse_source_spec("lyssach-nas:/Videoschnitt/Luma Fusion-Export")  # → RcloneSource
parse_source_spec("/Volumes/Card")                                  # → Path
parse_source_spec("~/Schnittprojekte")                              # → Path (expandiert)

NFC-Normalisierung: macOS speichert Dateinamen typisch als NFD (ä als a + Combining Diaeresis), Linux/SMB-NAS als NFC (precomposed ä). Beim Filename-Match in source_search_roots werden Namen via unicodedata.normalize("NFC", name) normalisiert, damit der Match auch bei gemischten Setups (macOS-Schnitt + Synology-NAS) funktioniert.

Unterstützte Formate

Format Status Anmerkung
*.fcpxml (Single-File) unterstützt Final Cut Pro und LumaFusion, Schema 1.x
*.fcpxmld/ (Bundle) unterstützt FCP-Konvention; Quelldateien direkt im Bundle
*.iMovieMobile (ZIP) unterstützt iOS-iMovie-Export; Datum, Auflösung, GPS, Titel direkt aus Plist
DaVinci, Premiere, Avid, native FCP-Library nicht geplant

Architektur

Schichten gemäss kurmann-python-api-Skill. Importrichtung strikt: cliapicoreservices. Services werden über Funktionsargumente injiziert, nicht direkt aus core importiert. Die Core-Funktionen read_first_clip_date und read_project_summary dispatchen nach erkanntem Format auf den jeweiligen Lese-Pfad.

src/schnittprojekt_leser/
├── api/                       # Public-Fassaden, fangen Exceptions zu error_message
├── cli/                       # Typer-Adapter, stdout/stderr-Disziplin
├── core/
│   ├── format_detection.py    # detect_format(path) → ProjectFormat
│   ├── recorded_at.py         # pick_recorded_at + Hierarchie + FCC-Fallback (FCPXML-Pfad)
│   ├── first_clip_date.py     # Komposition: Dispatch FCPXML / iMovieMobile
│   └── project_summary.py     # Komposition: Dispatch FCPXML / iMovieMobile
├── services/
│   ├── fcpxml_reader.py       # lxml → FcpxmlProject
│   ├── imovie_mobile_reader.py# zipfile + plistlib → IMovieMobileProject
│   ├── exiftool_runner.py     # subprocess → DateSources
│   ├── rclone_source.py       # rclone-Subprocess + NFC
│   └── source_resolver.py     # Lokale + rclone-Suche, Temp-Cleanup
└── models/                    # Datenmodelle, Enums, Errors

Drei-Kanal-Events:

  • Result (Pflicht) – fachliches Endergebnis, Exceptions als error_message.
  • on_event – strukturierte Stages (SchnittprojektLeserEvent mit Englisch-Stage-IDs gemäss ADR-0009, Deutsch-message).
  • on_output – roher Subprozess-Text (exiftool/rclone). Nur ans Terminal bei --verbose.

Scope von include_clips (v0.5.0+)

read_project_summary(..., include_clips=True) liefert das Inventar ausschliesslich Video-Originalmedien des Schnittprojekts. Konkret:

  • FCPXML: Top-Level-Spine-Asset-Clips mit is_real_source=True (Asset hat eine echte src-Quelldatei, kein Title/Generator).
  • iMovieMobile: editList-Einträge mit clipType=1 (Video). Hinweis: die Quelldateien liegen im selben ZIP-Bundle wie das Plist; in v0.5.0 wird keine medien-leser-Auflösung für Bundle-interne Pfade gemacht — resolution_status="skipped", media_metadata=None. Das Aufnahmedatum kommt für iMovieMobile aus dem Plist selbst (Recording-Info), das Clip-Inventar dient hier als Pfad-Referenz.

Bewusst nicht enthalten (Stand v0.5.0):

  • Audio-Subspuren oder separate Audio-Synchronspuren
  • Foto-Clips (clipType=5 in iMovieMobile)
  • Transitions (clipType=3)
  • Sekundäre Tracks / Picture-in-Picture
  • Generators, Titles, Effekte ohne Asset-Backing

Hintergrund: die Detail-Metadaten pro Clip werden vom kurmann-medien-leser gelesen, der heute auf Video-Metadaten (Codec, Auflösung, GPS, Kamera-Modell, Production) ausgerichtet ist. Audio-spezifische Schemas (id3, Tonmeister-Tags) sind dort noch nicht implementiert. Sobald der medien-leser Audio-Support hat, wird include_audio_tracks=True als additives Flag ergänzt — kein Schema-Bruch nötig.

Use-Case für Konsumenten: das Clip-Inventar erlaubt Cross-References zwischen Schnittprojekten und Originalmedien-Volumes (z.B. ISO-Manifests aus kamera-einleser). Fragen wie „welche Clips aus ISO XYZ wurden in Projekt ABC verwendet?" sind mit beiden Manifesten gemeinsam beantwortbar. Der geplante schnittprojekt-archivar wird include_clips=True für die clips.jsonl-Persistierung pro archiviertem Projekt nutzen.

Geteilte Auslese-Logik via kurmann-medien-leser (ab 0.4.0)

Seit 0.4.0 delegiert die Datums-Extraktion an kurmann-medien-leser — eine geteilte Library für Medien-Metadaten via exiftool, mit Best-Guess- Aufnahmedatum und Provenance. Was hier im Repo bleibt:

  • Konfidenz-Bewertung (ClipDateConfidence.HIGH/MEDIUM/LOW/UNKNOWN) — schnittprojekt-spezifisches Mapping vom medien-leser-source_label zur Vier-Stufen-Skala. Der familienfilm-manager nutzt das, um nur vertrauenswürdige Lookups (HIGH/MEDIUM) zu akzeptieren.
  • Schnittprojekt-Sentinels (SOURCE_PROJECT_FALLBACK, SOURCE_IMOVIE_EDITLIST_CREATION_DATE) — Datums-Quellen auf Projekt- Ebene, die kein exiftool brauchen.
  • Signatur-Anpassung + Exception-Mapping in services/exiftool_runner.py: Konsumenten unserer Library übergeben RuntimeOptions (mit exiftool_path und exiftool_timeout_seconds); medien-leser-API nimmt Keyword-Args. Exceptions werden auf unsere SchnittprojektLeserError- Hierarchie gemappt.

Andere Konsumenten der medien-leser-Library: kamera-einleser (für ISO-Manifests), geplant schnittprojekt-archivar (für Clip-Inventar in archivierten Schnittprojekten).

Änderungsverlauf

Die letzten drei Versionen:

  • 0.5.2 (2026-05-14) – Patch: zieht den kurmann-medien-leser-0.3.0-BMD-Fix verbindlich mit. Bei Blackmagic-Camera-Clips fehlten bisher viele camera.*- und production.*-Felder im Clip-Inventar (Mapping-Bug seit medien-leser v0.1.0). Pin von >=0.2,<0.3 auf >=0.3,<1.0 verschärft.
  • 0.5.1 (2026-05-21) – Patch: __version__-Konstante in __init__.py synchron mit pyproject.toml-Version. Bei 0.3.0–0.5.0 war sie bei "0.3.0" stehengeblieben (PyPI-Metadaten und API-Funktionalität waren in allen Versionen korrekt).
  • 0.5.0 (2026-05-21) – API-Konsolidierung: read_first_clip_date als Top-Level-Operation entfernt, Recording-Info ist Teil von read_project_summary als ProjectRecordingInfo. Neu: include_clips=True liefert das vollständige Inventar der referenzierten Video-Originalmedien mit MediaFileMetadata pro Clip. CLI-Subcommand inspect first-clip-date entfernt; inspect summary --include-clips ist der einzige Weg. Breaking Change auf API-Ebene; Konsumenten (z.B. familienfilm-manager) benötigen kleinen Patch.

Vollständige Historie: CHANGELOG.md.

Konsumenten

Lizenz

MIT. Siehe LICENSE.


Specs.md im Repo ist die ursprüngliche Spec als Starthilfe und wird zukünftig archiviert; die kanonische Doku der Fachlogik ist diese README.

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_schnittprojekt_leser-0.5.2.tar.gz (54.1 kB view details)

Uploaded Source

Built Distribution

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

kurmann_schnittprojekt_leser-0.5.2-py3-none-any.whl (49.3 kB view details)

Uploaded Python 3

File details

Details for the file kurmann_schnittprojekt_leser-0.5.2.tar.gz.

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.5.2.tar.gz
Algorithm Hash digest
SHA256 18fdda01e165b6abe0570e9d21cafafacc3d4d4cb4e8fb7baf982e59d7f1807f
MD5 faa02226f77ac1b42639236ee16f129a
BLAKE2b-256 68c7695a8c272814bbae1dc0326d2866ba5a41b2528d45805dd4912921fa330c

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.5.2.tar.gz:

Publisher: publish.yml on kurmann/schnittprojekt-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_schnittprojekt_leser-0.5.2-py3-none-any.whl.

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f6f922e11e50a9b3b8a194cfe551ea3e195dffa0f8e2fea8e7ceb1ea13a69bb7
MD5 a14ae8dbb9bb65d28d1d35efbb0a1ae0
BLAKE2b-256 c812312a4f756b45083e0244fced77a513e9d1dda3ef0ad2e3582b7544ec59e9

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.5.2-py3-none-any.whl:

Publisher: publish.yml on kurmann/schnittprojekt-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