Metadata-Version: 2.4
Name: kurmann-mediaset-creator
Version: 1.4.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: python-ulid>=2
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 ist ein ULID-basiertes Verzeichnis mit statischem HTML, Vorschaubildern,
ZIP-Download und der Videodatei – bereit zum Hochladen auf einen Webserver.

---

## 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
  (konfigurierbar via `vorschaubild-manager config set claude.api_key <key>`)

---

## Installation

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

Im Entwicklungsmodus (editierbar):

```bash
uv sync
```

---

## Verwendung (CLI)

### Einzelnes Video

```bash
mediaset-creator create /pfad/zu/video.m4v \
  --title "Familienfilme 2024" \
  --video-title "Leah Treppen-Surfen" \
  --video-description "Leah demonstriert ihre hohe Kunst des Treppen-Surfens." \
  --video-category "Familie Kurmann-Glück" \
  --video-date "2024-07-01" \
  --video-quality "Dolby Vision"
```

Das Medienset wird im Verzeichnis der Quelldatei unter einem ULID-Unterverzeichnis erstellt.
Ein anderes Ausgabeverzeichnis kann mit `--output-dir` angegeben werden:

```bash
mediaset-creator create /pfad/zu/video.m4v \
  --output-dir /tmp/mediasets \
  --video-title "Mein Film"
```

### Mehrere Videos (JSON)

Für Mediensets mit mehreren Videos wird eine JSON-Datei mit Metadaten übergeben:

```bash
mediaset-creator create --from-json metadaten.json
```

Format der JSON-Datei:

```json
{
  "title": "Familienfilme 2024",
  "videos": [
    {
      "path": "/pfad/zu/video1.m4v",
      "title": "Leah Treppen-Surfen",
      "description": "Beschreibung des Videos.",
      "category": "Familie Kurmann-Glück",
      "recording_date": "2024-07-01",
      "quality_label": "Dolby Vision",
      "frame_number": 500,
      "crop_position": "center-left"
    },
    {
      "path": "/pfad/zu/video2.m4v",
      "title": "Herbst-Spaziergang",
      "description": "Ein sonniger Herbsttag.",
      "recording_date": "2024-11-01",
      "quality_label": "4K HDR",
      "timestamp_seconds": 45.0
    }
  ]
}
```

### Optionen

| Option | Beschreibung |
|--------|--------------|
| `--title TEXT` | Übergeordneter Mediaset-Titel (h1). Bei Einzelvideo auch als Video-Titel verwendet, wenn `--video-title` fehlt. Fallback: Dateiname. |
| `--video-title TEXT` | Titel des Videos (nur bei Multi-Video nötig; bei Einzelvideo wird `--title` übernommen) |
| `--video-description TEXT` | Beschreibung des Videos |
| `--video-category TEXT` | Kategorie (z.B. «Familie Kurmann-Glück») |
| `--video-date TEXT` | Aufnahmedatum im ISO-Format (`YYYY-MM-DD`). Wird als Dateiname-Prefix und in der Anzeige als deutsches Long-Datum verwendet. |
| `--video-quality TEXT` | Qualitätsangabe (z.B. «Dolby Vision», «4K HDR») |
| `--output-dir PATH` | Basisverzeichnis für die Ausgabe (Default: Verzeichnis der Quelldatei) |
| `--from-json PATH` | JSON-Datei mit Metadaten für Multi-Video-Mediaset |
| `--ulid TEXT` | Bestehende ULID verwenden (überschreibt ein vorhandenes Medienset) |
| `--poster-frame N` | Video-Framenummer für Vorschaubild (überspringt KI-Auswahl) |
| `--poster-at SEKUNDEN` | Zeitpunkt in Sekunden für Vorschaubild-Frame (z.B. `90` oder `1.5`) |
| `--poster-crop POS` | Bildausschnitt für Poster (`left`/`center-left`/`center`/`center-right`/`right`) |
| `--force` | Erzwingt Neuerstellung aller Dateien, auch wenn Video bereits vorhanden ist |
| `--no-og-tags` | OpenGraph-Tags deaktivieren |
| `--verbose`, `-v` | Zusätzliche Ablaufinformationen auf stderr ausgeben |

### Ausgabe

stdout enthält den Pfad zum erstellten ULID-Verzeichnis:

```
/pfad/zum/output/01JNXYZ.../
```

Statusmeldungen werden auf stderr ausgegeben (nur mit `--verbose`).

### Erzeugte Verzeichnisstruktur

```
<ULID>/
├── index.html              # HTML-Seite mit Vorschaubild und Video-Link
├── video.mp4               # Video (komprimiert oder Original-Kopie)
├── video.jpg               # Vorschaubild (16:9, 1920×1080)
└── video.zip               # ZIP für Infuse (Video, -fanart.jpg, -poster.jpg, .nfo)
```

---

## 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 |
|-----------|--------------|---------|
| `og.base_url` | Stamm-URL für OG-Tags (z.B. `https://example.com/shares/`) | *(leer)* |
| `og.enabled` | OG-Tags aktivieren (`true`/`false`) | `true` |
| `og.site_name` | OG `site_name` Metadatum | *(leer)* |
| `og.locale` | OG `locale` Metadatum | `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 (ohne Erweiterung) als Titel-Fallback verwenden (`true`/`false`) | `true` |
| `filename.date_prefix` | Datum (YYYY-MM-DD) als Dateiname-Prefix verwenden (`true`/`false`) | `true` |
| `video.auto_compress` | Automatische Komprimierung bei >4K-UHD 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 – darüber wird komprimiert/nachkomprimiert | `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)* |

### Beispiel `config.toml`

```toml
[og]
base_url = "https://kurmannmedia.blob.core.windows.net/kurmann-glueck/"
enabled = "true"
site_name = "Patrick Kurmann Familienfilm-Freigabe"
locale = "de_CH"

[thumbnails]
portrait_suffix = "-poster"
sidecar = "true"

[title]
filename_fallback = "true"

[filename]
date_prefix = "true"

[video]
auto_compress = "true"
crf = "20"
max_bitrate = "40"
preset = "slow"

[tools]
ffmpeg = "ffmpeg"
ffprobe = "ffprobe"
nice_level = "10"
```

### OG-Tags

Wenn `og.base_url` gesetzt ist und OG-Tags aktiviert sind, generiert das HTML OpenGraph-Metadaten
für ansprechende Link-Vorschauen in Messengern und sozialen Netzwerken. Die vollständige URL
wird aus `base_url + ULID/` zusammengesetzt.

OG-Tags lassen sich deaktivieren via:
- `mediaset-creator config set og.enabled false` (persistent)
- `mediaset-creator create --no-og-tags ...` (pro Aufruf)

### Automatische Videokompression

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

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

**Was wird wohin geschrieben:**

| Datei | Inhalt |
|---|---|
| `video.mp4` im ULID-Verzeichnis | Komprimierte MP4 zum direkten Streaming |
| `video.zip` | **Original**-Videodatei + Vorschaubilder (für Medienserver) |

Wenn keine Kompression nötig ist, wird das Original wie bisher kopiert.

**Warum Software-Encoding statt Hardware-Encoding?**

Die komprimierte MP4-Datei dient dem Streaming via HTML über einen geheimen Link. Hier zählt
bestmögliche Qualität pro Bit, nicht Encoding-Geschwindigkeit. `libx265` (Software) erreicht
bei gleicher Bitrate ca. 20–30% bessere Qualität als VideoToolbox (Hardware), insbesondere
bei Szenen mit viel Bewegung (z.B. Schneefall, Regen, wehende Blätter). Der CRF-Modus
(Constant Rate Factor) gewährleistet konstante visuelle Qualität über das gesamte Video –
einfache Szenen werden kleiner, komplexe Szenen bekommen mehr Bitrate.

Das Original-Video bleibt unverändert im ZIP für den Download (z.B. für Infuse/Medienserver).
Durch die Skalierung von z.B. 4K auf 2560×1440 ist die Encoding-Geschwindigkeit trotz
Software-Encoding akzeptabel (~0.5–0.7x Echtzeit).

**Komprimierungsparameter:**

| Parameter | Wert | Konfigurierbar |
|-----------|------|----------------|
| Codec | HEVC (`libx265`), 10-Bit (`yuv420p10le`) | – |
| Qualität | CRF 20 (visuell verlustfrei) | `video.crf` |
| Preset | `slow` (beste Qualität/Grösse) | `video.preset` |
| Auflösung | 2560×1440 (QHD), Lanczos-Skalierung | – |
| Audio | AAC, 192 kbit/s | – |
| Streaming | `faststart` (Moov-Atom am Dateianfang) | – |
| HDR | Farbmetadaten (BT.2020, SMPTE 2084) werden durchgereicht | – |

**Nachkomprimierung bei hoher Bitrate:**

Wenn die Bitrate nach der initialen Komprimierung noch über 40 Mbit/s liegt (z.B. bei Videos
mit viel Bewegung wie Schneefall), wird automatisch erneut vom Original komprimiert – mit
schrittweise erhöhtem CRF (20 → 22 → 24 → ...) bis die Bitrate unter dem Schwellwert
liegt oder die CRF-Obergrenze von 28 erreicht ist.

**Fallback:** Falls `libx265` nicht verfügbar ist (z.B. bei einer ffmpeg-Installation ohne
`--enable-libx265`), wird automatisch auf VideoToolbox (macOS Hardware-Encoding) zurückgefallen.
Prüfen ob libx265 verfügbar ist: `ffmpeg -encoders 2>&1 | grep libx265`

**Komprimierung deaktivieren:**
```bash
mediaset-creator config set video.auto_compress false
```

**CPU-Drosselung** (verhindert Lüfterlärm bei lang laufenden Encodings):
```bash
mediaset-creator config set tools.nice_level 10
```

Der `nice`-Wert (0–19) steuert die Prozesspriorität von ffmpeg. Höhere Werte = geringere
Priorität. Leer lassen (Standard) bedeutet keine Drosselung.

---

## Verwendung (Python API)

Die öffentliche API kann von anderen Python-Anwendungen genutzt werden:

```python
from pathlib import Path
from mediaset_creator.api import (
    CreateMediasetRequest,
    MediaItem,
    RuntimeOptions,
    create_mediaset,
)

# Fachlicher Request
request = CreateMediasetRequest(
    items=[
        MediaItem(
            source_path=Path("/pfad/zu/video.m4v"),
            title="Leah Treppen-Surfen",
            description="Beschreibung des Videos.",
            category="Familie Kurmann-Glück",
            recording_date="1. Juli 2024",
            quality_label="Dolby Vision",
        ),
    ],
    mediaset_title="Familienfilme 2024",
    output_dir=Path("/tmp/mediasets"),
    enable_og_tags=True,
)

# Technische Laufzeitoptionen (getrennt vom fachlichen Request)
runtime = RuntimeOptions(
    base_url="https://example.com/shares/",
    ffmpeg_path="ffmpeg",
    ffprobe_path="ffprobe",
)

# Mediaset erstellen
result = create_mediaset(request, runtime)

if result.success:
    print(f"Mediaset erstellt: {result.output_dir}")
    print(f"HTML: {result.html_path}")
else:
    print(f"Fehler: {result.error_message}")
```

### Fortschritts-Events

Für lang laufende Operationen kann ein Event-Callback übergeben werden. Events sind
strukturierte Datenstrukturen (nicht Freitext) mit stabilen `stage_id`-Strings, die eine
Host-Applikation maschinell auswerten kann.

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

def on_event(event: MediasetCreatorEvent) -> None:
    label = f" [{event.current}/{event.total}]" if event.current is not None else ""
    print(f"  {event.message}{label}")

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

**Event-Reihenfolge pro Video:**

| Stage-ID | Beschreibung | Wann |
|----------|-------------|------|
| `mediaset_gestartet` | Mediaset-Erstellung beginnt | Einmal zu Beginn |
| `video_analysiert` | Videoanalyse (Auflösung, Bitrate, Codec) | Pro Video |
| `thumbnails_erstellt` | Breitbild- und Poster-Vorschaubilder erstellt | Pro Video |
| `nfo_erstellt` | Infuse/Firecore-Metadatei erstellt | Pro Video |
| `video_komprimiert` | Komprimierung abgeschlossen (mit CRF, Bitrate, Encoder) | Pro Video (wenn nötig) |
| `video_kopiert` | Original kopiert (keine Komprimierung nötig) | Pro Video (alternativ) |
| `video_nachkomprimiert` | Nachkomprimierung läuft (Bitrate noch zu hoch) | 0–n mal pro Video |
| `zip_erstellt` | ZIP-Archiv mit Original + Vorschaubilder erstellt | Pro Video |
| `html_generiert` | HTML-Seite mit OG-Tags generiert | Einmal am Ende |
| `mediaset_abgeschlossen` | Alle Dateien erstellt | Einmal am Ende |

**Hinweis zur Reihenfolge:** Thumbnails und NFO werden bewusst **vor** der Videokomprimierung
erstellt. So kann eine Host-Applikation nach `thumbnails_erstellt` + `nfo_erstellt` bereits
mit dem Infuse-Deployment beginnen (Original-Video + Vorschaubilder + NFO auf den
Medienserver kopieren), während die langsame Software-Komprimierung noch läuft.

**Komprimierungs-Zusammenfassung** (`video_komprimiert`) wird in der CLI immer auf `stderr`
angezeigt (auch ohne `--verbose`), z.B.:
```
Video komprimiert: video.mp4 | CRF=20 | 35 Mbit/s [1/1]
```

### Öffentliche API-Exporte

```python
from mediaset_creator.api import (
    create_mediaset,          # Hauptfunktion
    CreateMediasetRequest,    # Fachlicher Request
    MediaItem,                # Ein Medienelement (aktuell: Video)
    RuntimeOptions,           # Technische Laufzeitoptionen
    MediasetResult,           # Ergebnis mit Pfaden
    MediasetCreatorEvent,     # Strukturiertes Fortschritts-Event
    MediasetCreatorStage,     # Stage-IDs (StrEnum)
)
```

---

## Änderungsverlauf

Beinhaltet die letzten drei Minorversionen.

### 1.4.0 – 2026-03-29

- Software-Encoding: libx265 mit CRF-Modus statt VideoToolbox Hardware-Encoding für bessere Qualität pro Bit. Automatischer Fallback auf VideoToolbox wenn libx265 nicht verfügbar.
- Vereinfachte Bildnamen: Vorschaubild heisst `<stem>.jpg` ohne Suffix, Portrait + NFO nur noch im ZIP
- Reihenfolge optimiert: Thumbnails + NFO vor Videokomprimierung (ermöglicht frühes Infuse-Deployment durch Host-Applikation)
- Neue Config-Keys: `video.crf`, `video.preset`, `thumbnails.portrait_suffix`
- Neue Event-Stage: `NFO_ERSTELLT`
- Standard-Bitrate-Schwellwert von 50 auf 40 Mbit/s gesenkt
- Frame-Auswahl: `--poster-frame`, `--poster-at`, `--poster-crop` für manuelle Vorschaubild-Steuerung
- Inkrementelle Verarbeitung: Vorhandene Videos werden übersprungen wenn Quelle nicht neuer (`--force` zum Übersteuern)

### 1.3.0 – 2026-03-28

- Internet-freundliche Dateinamen: Umlaute transliteriert (ö→oe), Leerzeichen → Bindestriche, nur ASCII-Zeichen. Verhindert 404-Fehler durch Unicode-Normalisierungs-Unterschiede (macOS NFD vs. Server NFC).
- ZIP-Inhalte behalten originale Dateinamen (Infuse-freundlich)
- Neue Option `--ulid` zum Überschreiben bestehender Mediensets
- Automatische Nachkomprimierung bei zu hoher Bitrate

### 1.2.0 – 2026-03-28

- Verbessertes HTML-Layout: Mediathek-Header, Datenschutzhinweis, Videodauer
- Infuse-Kompatibilität: NFO-Datei im Firecore-Format
- Grösseres Play-Icon mit Drop-Shadow
- Erstellungsdatum (Timestamp) wird auf der HTML-Seite angezeigt
- Videodauer wird via ffprobe extrahiert und im HTML dargestellt

### 1.1.0 – 2026-03-27

- Automatische Videokompression: Bei >4K-UHD oder >50 Mbit/s wird eine komprimierte HEVC-MP4
  (2560×1440, VideoToolbox) erstellt – das Original kommt ins ZIP für den Medienserver
- CPU-Drosselung via `tools.nice_level` (0–19) verhindert Lüfterlärm bei lang laufenden Encodings
- Graceful Fallback: Bei Analysefehler oder fehlgeschlagenem Encoding wird das Original kopiert
- Neue Config-Keys: `video.auto_compress`, `tools.nice_level`
- Neue Event-Stages: `VIDEO_ANALYSIERT`, `VIDEO_KOMPRIMIERT`

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

---

## Lizenz

MIT
