Metadata-Version: 2.4
Name: kurmann-mediaset-creator
Version: 3.1.0
Summary: CLI-Tool zur Erstellung von Mediensets (statisches HTML mit OG-Tags, Vorschaubildern und ZIP-Download) aus Videodateien.
Project-URL: Homepage, https://github.com/kurmann/mediaset-creator
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: jinja2>=3
Requires-Dist: kurmann-vorschaubild-manager>=2.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"

# mediaset-creator

CLI-Tool und Python-Bibliothek zur Erstellung von Mediensets aus Videodateien.
Ein Medienset repräsentiert genau ein Video und kann als Infuse-Medienset (Original-Video +
Artworks + Metadaten) und/oder Web-Medienset (HTML-Seite + komprimiertes Video + ZIP) in
separate Verzeichnisse ausgegeben werden.

---

## Voraussetzungen

- Python 3.11+
- [ffmpeg](https://ffmpeg.org/) und `ffprobe` im `$PATH`
- [kurmann-vorschaubild-manager](https://pypi.org/project/kurmann-vorschaubild-manager/) (wird als Dependency installiert)
- Ein [Anthropic API Key](https://console.anthropic.com/) für die automatische
  Vorschaubildauswahl (siehe [API-Keys](#api-keys-ki-funktionen))

---

## Installation

```bash
uv pip install kurmann-mediaset-creator
```

Im Entwicklungsmodus (editierbar):

```bash
uv sync
```

---

## Konzept: Profile

Ein Medienset kann in zwei Profilen ausgegeben werden:

| Profil | Zweck | Inhalt |
|--------|-------|--------|
| **Infuse** | Medienserver (Infuse/Firecore) | Original-Video, Landscape (-fanart), Portrait (-poster), NFO |
| **Web** | Streaming via Hidden Link | HTML-Seite, komprimiertes Video, Landscape, optional ZIP |

Jedes Profil schreibt in ein eigenes Verzeichnis. Beide Profile sind unabhängig aktivierbar –
mindestens eines muss angegeben werden.

### Ausgabestruktur

```
infuse_dir/                              web_dir/{ULID}/
├── Leah Treppen-Surfen.m4v              ├── index.html
├── Leah Treppen-Surfen-fanart.jpg       ├── leah-treppen-surfen.jpg
├── Leah Treppen-Surfen-poster.jpg       ├── leah-treppen-surfen.mp4  (komprimiert)
└── Leah Treppen-Surfen.nfo              └── leah-treppen-surfen.zip  (optional)
```

- **Infuse:** Dateien mit Original-Dateinamen (inkl. Umlaute, Leerzeichen)
- **Web:** Dateien mit sanitisierten ASCII-Dateinamen, ULID-Unterverzeichnis für Hidden Links

---

## Verwendung (CLI)

```bash
# Einfachster Aufruf – Web-Medienset im Quellverzeichnis
mediaset-creator create /pfad/zu/video.m4v \
  --title "Leah Treppen-Surfen" \
  --date "2024-07-01"

# Mit expliziten Profilen
mediaset-creator create /pfad/zu/video.m4v \
  --title "Leah Treppen-Surfen" \
  --category "Familie Kurmann-Glück" \
  --date "2024-07-01" \
  --infuse-dir /nas/infuse/familienfilme \
  --web-dir /nas/shares
```

Ohne `--infuse-dir` oder `--web-dir` wird automatisch ein **Web-Medienset im Verzeichnis
der Quelldatei** erstellt (Web-ID-Unterordner).

### Optionen

| Option | Profil | Beschreibung |
|--------|--------|-------------|
| `VIDEO` (Argument) | Pflicht | Pfad zur Videodatei |
| `--title TEXT` | Gemeinsam | Titel des Videos (auch H1-Überschrift). Fallback: Dateiname. |
| `--mediaset-title TEXT` | Gemeinsam | Optionaler H1-Titel, falls abweichend vom Video-Titel |
| `--description TEXT` | Gemeinsam | Beschreibung des Videos |
| `--category TEXT` | Gemeinsam | Kategorie (z.B. «Familie Kurmann-Glück») |
| `--date TEXT` | Gemeinsam | Aufnahmedatum im ISO-Format (`YYYY-MM-DD`) |
| `--max-luminance N` | Gemeinsam | Peak-Leuchtdichte in nits für HDR-Metadaten (Default: 1000) |
| `--poster-frame N` | Gemeinsam | Video-Framenummer für Vorschaubild |
| `--poster-at SEKUNDEN` | Gemeinsam | Zeitpunkt in Sekunden für Vorschaubild-Frame |
| `--poster-crop POS` | Gemeinsam | Bildausschnitt für Poster (`left`/`center`/`right`/...) |
| `--work-dir PATH` | Gemeinsam | Arbeitsverzeichnis für temporäre Dateien. Default: Quellverzeichnis |
| `--force` | Gemeinsam | Erzwingt Neuerstellung aller Dateien |
| `--verbose`, `-v` | Gemeinsam | Zusätzliche Ablaufinformationen auf stderr |
| `--infuse-dir PATH` | **Infuse** | Aktiviert Infuse-Profil, Zielverzeichnis |
| `--web-dir PATH` | **Web** | Web-Profil Basisverzeichnis (+ Web-ID). Default: Quellverzeichnis |
| `--web-id TEXT` | **Web** | Web-ID übersteuern (12 Zeichen, a-z 0-9). Default: auto-generiert |
| `--no-zip` | **Web** | Kein ZIP im Web-Profil |
| `--no-og-tags` | **Web** | OpenGraph-Tags deaktivieren |

### Ausgabe (stdout)

- Nur Infuse: Pfad zum Infuse-Verzeichnis
- Nur Web (oder Default): Pfad zum ULID-Verzeichnis
- Beide: JSON `{"infuse": "/pfad/...", "web": "/pfad/.../ULID"}`

---

## Konfiguration

Einstellungen werden in `~/.config/mediaset-creator/config.toml` gespeichert.

### Befehle

```bash
# Wert speichern
mediaset-creator config set <schlüssel> "<wert>"

# Einzelnen Wert lesen
mediaset-creator config get <schlüssel>

# Alle gespeicherten Werte anzeigen
mediaset-creator config list
```

### Erlaubte Schlüssel

| Schlüssel | Beschreibung | Standard |
|-----------|--------------|---------|
| `branding.base_url` | Stamm-URL für Web-Mediasets und OG-Tags (z.B. `https://mediathek.example.com/`) | *(leer)* |
| `branding.enabled` | OpenGraph-Meta-Tags rendern (`true`/`false`) | `true` |
| `branding.site_name` | Site-Name (sichtbar im Header + OG-Meta) | *(leer)* |
| `branding.locale` | OG `locale` Metadatum (z.B. `de_CH`) | `de_CH` |
| `thumbnails.portrait_suffix` | Suffix für Hochformat-Vorschaubilder (Infuse: `-poster`) | `-poster` |
| `thumbnails.sidecar` | Sidecar-Bilder als Landscape-Quelle verwenden (`true`/`false`) | `true` |
| `title.filename_fallback` | Dateiname als Titel-Fallback (`true`/`false`) | `true` |
| `filename.date_prefix` | Datum (YYYY-MM-DD) als Dateiname-Prefix (`true`/`false`) | `true` |
| `video.auto_compress` | Automatische Komprimierung bei >4K oder >40 Mbit/s (`true`/`false`) | `true` |
| `video.crf` | CRF-Wert für libx265 (0–51, tiefer = bessere Qualität) | `20` |
| `video.max_bitrate` | Maximale Bitrate in Mbit/s | `40` |
| `video.preset` | libx265-Preset (`ultrafast`/`fast`/`medium`/`slow`/`veryslow`) | `slow` |
| `tools.ffmpeg` | Pfad zur `ffmpeg`-Binärdatei | `ffmpeg` |
| `tools.ffprobe` | Pfad zur `ffprobe`-Binärdatei | `ffprobe` |
| `tools.nice_level` | CPU-Priorität für ffmpeg via `nice` (0–19; leer = keine Drosselung) | *(leer)* |

### Automatische Videokompression

Wenn `video.auto_compress` aktiv ist (Standard), analysiert das Tool die technischen Eigenschaften
des Videos. Bei Überschreiten eines der folgenden Schwellwerte wird automatisch eine komprimierte
MP4-Version für das Web-Profil erstellt:

- **Auflösung > 4K UHD** (> 3840×2160)
- **Bitrate > 40 Mbit/s** (konfigurierbar via `video.max_bitrate`)

Das Infuse-Profil erhält immer das **Original-Video** – Komprimierung betrifft nur das Web-Profil.

| Parameter | Wert | Konfigurierbar |
|-----------|------|----------------|
| Codec | HEVC (`libx265`), 10-Bit | – |
| Qualität | CRF 20 | `video.crf` |
| Preset | `slow` | `video.preset` |
| Auflösung | 2560×1440 (QHD), Lanczos | – |
| Audio | AAC, 192 kbit/s | – |

**Fallback:** Falls `libx265` nicht verfügbar, wird auf VideoToolbox (macOS) zurückgefallen.

---

## API-Keys (KI-Funktionen)

Der Mediaset-Creator selbst spricht **nicht** mit Claude. Wenn ein
`PosterSpec` ohne `frame_number`/`timestamp_seconds` (oder ganz `None`)
übergeben wird, ruft er den
[`vorschaubild-manager`](https://github.com/kurmann/vorschaubild-manager)
auf, der die Frame-Auswahl und/oder den Crop-Vorschlag via Claude erledigt.

### Empfohlenes Setup: `ANTHROPIC_API_KEY` als Umgebungsvariable

```bash
# In ~/.zshenv eintragen (gilt für interaktive UND non-interaktive Shells,
# also auch für launchd / cron / Tool-Subprozesse via uv tool):
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshenv

# Für laufende launchd-Jobs zusätzlich (einmalig, persistiert über Reboots):
launchctl setenv ANTHROPIC_API_KEY "sk-ant-..."
```

`ANTHROPIC_API_KEY` ist der **offizielle** Variablenname des Anthropic-SDK
und wird vom Vorschaubild-Manager bevorzugt gelesen. Der historische Name
`CLAUDE_API_KEY` bleibt als Fallback unterstützt.

### Wer liest den Key wann?

| Tool | Liest Key? | Wann |
|------|-----------|------|
| Mediaset-Creator (dieses Tool) | ❌ nein | Pass-Through, KI greift implizit auf das Prozess-ENV zu |
| Vorschaubild-Manager | ✅ ja | beim Mosaic-Auto-Frame und beim Crop-Auto-Wahl |

### Alternative: lokale Config-Datei

```bash
vorschaubild-manager config set claude.api_key "sk-ant-..."
# → schreibt nach ~/.config/vorschaubild-manager/config.toml
```

### Ohne API-Key

Wenn weder ENV noch Config gesetzt sind und kein expliziter
`PosterSpec.frame_number`/`timestamp_seconds` übergeben wurde, schlägt
die Mediaset-Erstellung mit einer Fehlermeldung aus dem Vorschaubild-Manager
fehl.

---

## Verwendung (Python API)

```python
from pathlib import Path
from mediaset_creator.api import (
    CreateMediasetRequest,
    InfuseProfile,
    PosterSpec,
    WebProfile,
    RuntimeOptions,
    create_mediaset,
)

request = CreateMediasetRequest(
    source_path=Path("/pfad/zu/video.m4v"),
    title="Leah Treppen-Surfen",
    description="Beschreibung des Videos.",
    category="Familie Kurmann-Glück",
    recording_date="2024-07-01",
    poster=PosterSpec(timestamp_seconds=5.0),
    mediaset_title="Familienfilme 2024",
    work_dir=Path("/tmp/mediaset-work"),  # Optional: Arbeitsverzeichnis für temporäre Dateien
    infuse=InfuseProfile(output_dir=Path("/nas/infuse/familienfilme")),
    web=WebProfile(
        output_dir=Path("/nas/shares"),
        include_zip=True,
        enable_og_tags=True,
    ),
)

runtime = RuntimeOptions(
    base_url="https://mediathek.example.com/",
)

result = create_mediaset(request, runtime)

if result.success:
    if result.infuse:
        print(f"Infuse: {result.infuse.output_dir}")
    if result.web:
        print(f"Web: {result.web.output_dir} (Web-ID: {result.web.web_id})")
else:
    print(f"Fehler: {result.error_message}")
```

### Fortschritts-Events

Events sind strukturierte Datenstrukturen mit stabilen `stage_id`-Strings.
Milestone-Events (`INFUSE_READY`, `WEB_READY`) enthalten ein `paths`-Dict mit den erzeugten
Dateipfaden, sodass Host-Applikationen sofort mit dem Deployment beginnen können.

```python
from mediaset_creator.api import MediasetCreatorEvent, MediasetCreatorStage

def on_event(event: MediasetCreatorEvent) -> None:
    print(f"  {event.message}")
    if event.stage_id == MediasetCreatorStage.INFUSE_READY:
        # Medienserver-Deployment starten
        deploy_to_infuse(event.paths)
    elif event.stage_id == MediasetCreatorStage.WEB_READY:
        # Web-Deployment starten
        deploy_to_web(event.paths)

result = create_mediaset(request, runtime, on_event=on_event)
```

### Event-Phasen

| Phase | Stage-ID | Beschreibung |
|-------|----------|-------------|
| – | `mediaset_started` | Mediaset-Erstellung beginnt |
| **1** | `video_analyzed` | Videoanalyse (Auflösung, Bitrate, Codec) |
| **1** | `thumbnails_created` | Landscape- und Portrait-Vorschaubilder erstellt |
| **1** | `nfo_created` | Infuse/Firecore-Metadatei erstellt |
| **2** | ⭐ `infuse_ready` | Infuse-Medienset komplett (mit `paths`) |
| **3** | `video_copied` | Original kopiert (keine Komprimierung nötig) |
| **3** | `video_skipped` | Video unverändert, übersprungen |
| **3** | `zip_created` | ZIP-Archiv erstellt |
| **3** | `html_generated` | HTML-Seite generiert |
| **4** | `video_recompressed` | Nachkomprimierung läuft (Bitrate zu hoch) |
| **4** | `video_compressed` | Komprimierung abgeschlossen |
| **5** | ⭐ `web_ready` | Web-Medienset komplett (mit `paths`) |
| **5** | `mediaset_completed` | Alle Profile fertig |

### MediasetResult

```python
@dataclass
class MediasetResult:
    success: bool
    infuse: InfuseResult | None    # Infuse-Profil Ergebnis
    web: WebResult | None          # Web-Profil Ergebnis
    error_message: str | None

@dataclass
class InfuseResult:
    output_dir: Path
    video_path: Path
    landscape_path: Path
    portrait_path: Path
    nfo_path: Path

@dataclass
class WebResult:
    output_dir: Path               # Web-ID-Verzeichnis
    web_id: str                    # 12 Zeichen, a-z 0-9 (Lowercase Base36)
    html_path: Path
    video_path: Path
    landscape_path: Path
    zip_path: Path | None          # None wenn include_zip=False
```

### Öffentliche API-Exporte

```python
from mediaset_creator.api import (
    create_mediaset,          # Hauptfunktion
    CreateMediasetRequest,    # Fachlicher Request
    InfuseProfile,            # Infuse-Profil Konfiguration
    WebProfile,               # Web-Profil Konfiguration
    PosterSpec,               # Vorschaubild-Parameter
    RuntimeOptions,           # Technische Laufzeitoptionen
    MediasetResult,           # Ergebnis (profil-basiert)
    InfuseResult,             # Infuse-Ergebnis
    WebResult,                # Web-Ergebnis
    MediasetCreatorEvent,     # Strukturiertes Fortschritts-Event
    MediasetCreatorStage,     # Stage-IDs (StrEnum)
)
```

---

## Änderungsverlauf

Beinhaltet die letzten drei Versionen.

### 3.0.0 – 2026-04-17

- **Web-ID statt ULID:** Web-Mediasets bekommen 12-stellige Lowercase-IDs (a-z, 0-9) statt 26-stellige ULIDs. Kürzere URLs (`/a3xk9mn2pqrw/` statt `/01KPCZDEZVQ0PDNAJKB6V5RTXV/`)
- **Dolby Vision-Erkennung:** Wird automatisch erkannt und im `color_space_label` ausgegeben (Vorrang vor "BT.2020 PQ")
- **HTML-Title mit Kategorie:** `<title>Familie Kurmann-Glück – Heubürzel Ann-Sophie</title>`
- **Technische Specs im HTML:** Stream + Original/ZIP mit Farbraum, Auflösung, fps, Bitrate
- **`VideoInfo.frame_rate` + `bit_depth` + `color_space_label` + `is_dolby_vision`:** Neue Properties
- **`VideoSpecs` Dataclass:** Neue View-Modell-Klasse mit `format_compact()`
- **`OgConfig` → `BrandingConfig`:** Klarerer Name, gleiche Felder (+ `web_id` statt `ulid`)
- **Config-Keys:** `og.X` → `branding.X`
- **`quality_label` entfernt:** Durch Tech-Specs überflüssig (Dolby Vision wird automatisch erkannt)
- **Breaking:** HTML-Generierung läuft jetzt nach der Komprimierung. `HTML_GENERATED` Event kommt nach `VIDEO_COMPRESSED`. `WEB_READY` bleibt unverändert als zentraler Milestone.
- **Dependency:** `python-ulid` entfernt

### 2.1.0 – 2026-04-16

- **HDR-Metadaten-Einbettung:** MaxCLL, MaxFALL und Mastering Display Metadaten werden automatisch in HDR-Videos eingebettet, damit Geräte die Peak-Leuchtdichte korrekt interpretieren
- **`--max-luminance`:** Peak-Leuchtdichte in nits übersteuern (Default: 1000)
- **HDR-Erkennung:** `VideoInfo.is_hdr` via ffprobe (PQ/HLG/BT.2020)
- **AudioToolbox-Encoder:** `aac_at` (macOS) für bessere Audioqualität

### 2.0.1 – 2026-04-09

- HTML-Template als Python-String eingebettet statt aus Dateisystem laden

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

---

## Lizenz

MIT
