Metadata-Version: 2.4
Name: kurmann-mediaset-creator
Version: 2.0.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 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
  (konfigurierbar via `vorschaubild-manager config set claude.api_key <key>`)

---

## 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" \
  --quality "Dolby Vision" \
  --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 (ULID-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`) |
| `--quality TEXT` | Gemeinsam | Qualitätsangabe (z.B. «Dolby Vision», «4K HDR») |
| `--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 (+ ULID). Default: Quellverzeichnis |
| `--ulid TEXT` | **Web** | ULID übersteuern (für bestehende Mediensets) |
| `--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 |
|-----------|--------------|---------|
| `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 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.

---

## 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",
    quality_label="Dolby Vision",
    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://example.com/shares/",
)

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} (ULID: {result.web.ulid})")
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               # ULID-Verzeichnis
    ulid: str
    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.

### 2.0.0 – 2026-04-01

- **Profil-basierte Ausgabe:** Mediensets werden in separate Verzeichnisse für Infuse (`--infuse-dir`) und Web (`--web-dir`) ausgegeben
- **Ein Medienset = ein Video:** Multi-Video-Logik und `--from-json` entfernt
- **Neuer API-Contract:** `MediaItem` entfernt, Video-Felder direkt auf `CreateMediasetRequest`. `MediasetResult` profil-basiert mit `InfuseResult`/`WebResult`. Poster-Parameter in `PosterSpec` gruppiert
- **Vereinfachte CLI-Optionen:** `--title` statt `--video-title`, `--category` statt `--video-category`, etc.
- **Milestone-Events mit Pfaden:** `INFUSE_READY` und `WEB_READY` enthalten `paths`-Dict
- **Site-Name konfigurierbar:** HTML-Header nutzt `og.site_name` aus Config statt Hardcoding
- **Breaking:** Alle `stage_id`-Werte von Deutsch auf Englisch

### 1.4.0 – 2026-03-29

- Software-Encoding: libx265 mit CRF-Modus
- Frame-Auswahl: `--poster-frame`, `--poster-at`, `--poster-crop`
- Inkrementelle Verarbeitung: `--force` zum Übersteuern

### 1.3.0 – 2026-03-28

- Internet-freundliche Dateinamen: Umlaute transliteriert, ASCII-sicher
- ZIP-Inhalte behalten originale Dateinamen (Infuse-freundlich)

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

---

## Lizenz

MIT
