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

Zwei Subcommands unter inspect:

schnittprojekt-leser inspect first-clip-date <pfad> [--format value|json|iso-datetime]
schnittprojekt-leser inspect summary           <pfad> [--format text|json]

Beispiel: Datum des ersten Clips als ISO-Datum (Pipeline-Default):

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" --timezone Europe/Zurich
2026-03-29

$ schnittprojekt-leser inspect first-clip-date "Mein Projekt.iMovieMobile" --timezone Europe/Zurich
2026-04-23

Beispiel: Vollzeitstempel:

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" \
    --timezone Europe/Zurich --format iso-datetime
2026-03-29T08:20:02+02:00

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

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" \
    --timezone Europe/Zurich --format json
{
  "success": true,
  "iso_date": "2026-03-29",
  "iso_datetime": "2026-03-29T08:20:02+02:00",
  "source_label": "exif:Keys:CreationDate",
  "confidence": "high",
  "project_format": "fcpxml_bundle",
  "resolved_source_file": "/.../Mein Schnitt.fcpxmld/A001_03290820_D498.mov",
  "error_message": null,
  "warnings": []
}

Beispiel: Projekt-Übersicht:

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxml"
Projekt: Mein Schnitt
Format: fcpxml_file (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

Beispiel: Quelldateien auf einem rclone-Remote suchen lassen:

schnittprojekt-leser inspect first-clip-date "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_first_clip_date, read_project_summary
from schnittprojekt_leser.models import (
    ReadFirstClipDateRequest,
    ReadProjectSummaryRequest,
    RuntimeOptions,
    RcloneSource,
)

# 1. Datum des ersten Clips
result = read_first_clip_date(
    ReadFirstClipDateRequest(
        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.iso_date, result.iso_datetime, result.confidence)
else:
    print("Fehler:", result.error_message)

# 2. Projekt-Übersicht (rein FCPXML-intern, kein Quelldatei-Zugriff)
summary = read_project_summary(
    ReadProjectSummaryRequest(project_path=Path("Mein Schnitt.fcpxml")),
)
print(summary.project_name, summary.video_resolution, summary.clip_count_primary)

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 rein auf Projekt + Timeline. read_first_clip_date ist eine Komposition Timeline → Asset → Quelldatei.

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.

Hinweis: Code-Spiegelung mit kamera-einleser

Die Datums-Extraktion (core/recorded_at.py, services/exiftool_runner.py, models/dates.py) und die rclone-Integration (services/rclone_source.py, RcloneSource-Dataclass, parse_source_spec) sind bewusst nahe an kamera-einleser gehalten: identische Tag-Liste, identische Hierarchie, identisches Source-Label-Format, identische rclone-Aufrufmuster, identische NFC-Normalisierung.

Aktuell als Redundanz akzeptiert. Bei einem dritten Konsumenten dieser Logik ist ein Extract zu einer gemeinsamen Bibliothek kurmann-recorded-at geplant – die API-Form ist exakt spiegelnd, damit der Extract mechanisch möglich ist.

Änderungsverlauf

Die letzten drei Versionen:

  • 0.3.0 (2026-05-08) – iMovieMobile-Support (*.iMovieMobile-ZIP-Bundles von iOS-iMovie). Aufnahmedatum direkt aus dem Plist (editList[i].creationDate, HIGH-Konfidenz), kein exiftool nötig. Format-Detection erweitert, Core-Dispatch FCPXML/iMovieMobile, neue Modelle IMovieMobileProject/ IMovieClip/IMovieMapLocation. 22 zusätzliche Tests.
  • 0.2.0 (2026-05-08) – Erste Implementierung Phase 1: FCPXML-Reader (LumaFusion + FCP), exiftool-basierte Datums-Auslesung mit 7-stufiger Hierarchie, FCC-TZ-Fallback, rclone-Quellen, NFC-Normalisierung, CLI mit inspect first-clip-date und inspect summary.
  • 0.1.0 (2026-05-08) – Initiale Spezifikation Specs.md als Starthilfe (siehe ADR-0010).

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.3.0.tar.gz (44.5 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.3.0-py3-none-any.whl (48.1 kB view details)

Uploaded Python 3

File details

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

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.3.0.tar.gz
Algorithm Hash digest
SHA256 26f01d297e6be5e3a744938dbc0104056e4424439a117ed03005f452b2bb1edf
MD5 6f18088e69783c767e37d1fc5d6d4a97
BLAKE2b-256 eb01b10f31a95bb91b7dc67d3530e16dfcdc397ac88d565037e65490e4553a4a

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.3.0.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.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f0704b7988eba495d4e486a64253027a089de55dd1492c3c861d16636b47f38
MD5 1ada0e58fe0ef3fcc58a6f12b48e13a2
BLAKE2b-256 52351faeb332214f8f74bbb8491efd83379933c839ec3223f44b3c7ca06ac609

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.3.0-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