Metadata-Version: 2.4
Name: kurmann-schnittprojekt-leser
Version: 0.2.0
Summary: Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten (FCPXML).
Author: Patrick Kurmann
License: MIT License
        
        Copyright (c) 2026 Patrick Kurmann
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/kurmann/schnittprojekt-leser
Project-URL: Repository, https://github.com/kurmann/schnittprojekt-leser
Project-URL: Changelog, https://github.com/kurmann/schnittprojekt-leser/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/kurmann/schnittprojekt-leser/issues
Keywords: video,fcpxml,luma-fusion,final-cut-pro,metadata
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: lxml>=5.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# kurmann-schnittprojekt-leser

Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten. Liest FCPXML
(Final Cut Pro und LumaFusion) 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`](https://github.com/kurmann/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](https://exiftool.org/)** – Pflicht. Die Bibliothek liest
  Aufnahmedaten aus den referenzierten Quelldateien (MOV/MP4/HEIC/JPG) via
  exiftool-Subprozess.
  - macOS: `brew install exiftool`
  - Linux: `sudo apt install libimage-exiftool-perl`
- **[rclone](https://rclone.org/)** – optional. Nur nötig, wenn
  `RuntimeOptions.source_search_roots` rclone-Remotes enthält (z. B.
  `lyssach-nas:/Videoschnitt/...`).

## Installation

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

Lokale Entwicklung (Repo geklont):

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

## Verwendung

### CLI

Zwei Subcommands unter `inspect`:

```bash
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):**

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

**Beispiel: Vollzeitstempel:**

```bash
$ 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`):**

```bash
$ 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:**

```bash
$ 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:**

```bash
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

```python
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

Spiegelt das Vorbild aus
[`kamera-einleser`](https://github.com/kurmann/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 steuert ausschliesslich,
in welcher Zone das `iso_date` (Wallclock-Tag) ausgewiesen wird, und wie naive
Zeitstempel interpretiert werden. `iso_datetime` bleibt der Originalwert mit
Offset, ohne Konvertierung.

| Stempel | Assumption | Verhalten |
|---|---|---|
| Aware (z. B. `…+02:00`) | gesetzt | Stempel in Assumption-TZ konvertieren, Datum daraus ableiten |
| Aware | `None` | Datum aus der Eigen-TZ des Stempels |
| Naive (kein Offset) | gesetzt | Stempel als Lokalzeit der Assumption interpretieren |
| Naive | `None` | Stempel als UTC interpretieren |

Für Familienfilme zählt der Wallclock-Tag. iPhone-Aufnahmen werden meist mit
`com.apple.quicktime.creationdate` plus Local-TZ-Offset gespeichert; ohne
Assumption führt `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:

```python
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 | Phase | Anmerkung |
|---|---|---|
| `*.fcpxml` (Single-File) | 1 | Final Cut Pro und LumaFusion |
| `*.fcpxmld/` (Bundle) | 1 | FCP-Konvention; Quelldateien direkt im Bundle |
| `*.iMovieMobile` (ZIP) | 2 | Sample-Analyse als Anhang K in [`Specs.md`](Specs.md); Implementierung folgt |
| DaVinci, Premiere, Avid, native FCP-Library | – | nicht unterstützt |

## Architektur

Schichten gemäss [`kurmann-python-api`](https://github.com/kurmann/atelier/tree/main/skills/kurmann-python-api)-Skill.
Importrichtung strikt: `cli` → `api` → `core` ← `services`. Services werden
über Funktionsargumente injiziert, nicht direkt aus `core` importiert.

```
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
│   ├── first_clip_date.py     # Komposition Detection → FCPXML → Resolver → exiftool
│   └── project_summary.py     # Komposition Detection → FCPXML
├── services/
│   ├── fcpxml_reader.py       # lxml → FcpxmlProject
│   ├── 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](https://github.com/kurmann/atelier/blob/main/decisions/0009-stage-ids-englisch.md),
  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`](https://github.com/kurmann/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.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`](Specs.md) als
  Starthilfe (siehe
  [ADR-0010](https://github.com/kurmann/atelier/blob/main/decisions/0010-specs-im-repo-sobald-repo-existiert.md)).

Vollständige Historie: [CHANGELOG.md](CHANGELOG.md).

## Konsumenten

- **[`familienfilm-manager`](https://github.com/kurmann/familienfilm-manager)** –
  Hauptmotivation. Holt Aufnahmedatum für Datei-Naming und Text-Overlay-Vorbereitung.
- Plausible spätere: [`fcplib-manager`](https://github.com/kurmann/fcplib-manager),
  [`mediaset-creator`](https://github.com/kurmann/mediaset-creator).

## Lizenz

MIT. Siehe [LICENSE](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.
