Add JSON configuration and path discovery

This commit is contained in:
Melbar
2026-05-07 18:27:02 +02:00
parent 184f56091b
commit 37310d9335
3 changed files with 198 additions and 25 deletions
+47 -7
View File
@@ -25,9 +25,9 @@
## Voraussetzungen
- Windows (getestet)
- FFmpeg + FFprobe (in `C:\Software\` oder Pfad in Script anpassen)
- FFmpeg + FFprobe im `PATH`, in `C:\Tools\FFMPEG` oder in `C:\Software`
- Python 3.10+
- Schreibzugriff auf den Ausgabeordner `H:\VOD`
- Schreibzugriff auf den Ausgabeordner `F:\VOD` oder `H:\VOD`
## Installation & Nutzung
@@ -129,14 +129,54 @@ Film_forced.srt
## Konfiguration
Die wichtigsten Pfade stehen oben in `pvd_mezzanine.py`:
Die wichtigsten Parameter stehen in `config.json`. Wenn die Datei fehlt, wird sie beim Start mit Default-Werten erzeugt.
```python
FFMPEG_EXE = r"C:\Software\ffmpeg.exe"
FFPROBE_EXE = r"C:\Software\ffprobe.exe"
OUTPUT_BASE_DIR = r"H:\VOD"
```json
{
"ffmpeg": {
"ffmpeg_exe": "",
"ffprobe_exe": "",
"search_dirs": [
"C:\\Tools\\FFMPEG",
"C:\\Software"
]
},
"output": {
"base_dir": "",
"preferred_dirs": [
"F:\\VOD",
"H:\\VOD"
]
},
"video": {
"hd_bitrate": "30M",
"hd_maxrate": "35M",
"hd_bufsize": "50M",
"sd_bitrate": "8M",
"sd_maxrate": "10M",
"sd_bufsize": "15M"
},
"audio": {
"mp4_bitrate": "256k",
"sample_rate": "48000",
"pcm_codec": "pcm_s24le"
}
}
```
FFmpeg wird in dieser Reihenfolge gesucht:
1. Explizite Pfade aus `ffmpeg_exe` und `ffprobe_exe`
2. `PATH`
3. `C:\Tools\FFMPEG`
4. `C:\Software`
Das Zielverzeichnis wird so bestimmt:
1. `output.base_dir`, wenn gesetzt
2. erstes vorhandenes Verzeichnis aus `output.preferred_dirs`, standardmaessig `F:\VOD`, dann `H:\VOD`
3. falls keines existiert, der erste Preferred-Dir-Wert
## Git-Hinweis
Die lokale `.env` enthält Zugangsdaten und wird absichtlich nicht versioniert. Neue Änderungen sollten zusammen mit einer passenden README-Aktualisierung committed und nach `main` gepusht werden.
+34
View File
@@ -0,0 +1,34 @@
{
"ffmpeg": {
"ffmpeg_exe": "",
"ffprobe_exe": "",
"search_dirs": [
"C:\\Tools\\FFMPEG",
"C:\\Software"
]
},
"output": {
"base_dir": "",
"preferred_dirs": [
"F:\\VOD",
"H:\\VOD"
]
},
"video": {
"hd_bitrate": "30M",
"hd_maxrate": "35M",
"hd_bufsize": "50M",
"hd_level": "4.1",
"sd_bitrate": "8M",
"sd_maxrate": "10M",
"sd_bufsize": "15M",
"sd_level": "3.1",
"preset": "slow",
"tune": "film"
},
"audio": {
"mp4_bitrate": "256k",
"sample_rate": "48000",
"pcm_codec": "pcm_s24le"
}
}
+115 -16
View File
@@ -5,6 +5,7 @@ import math
import os
import queue
import re
import shutil
import subprocess
import sys
import threading
@@ -25,9 +26,102 @@ except Exception:
# =============================================================================
# 1. KONFIGURATION UND PFADE
# =============================================================================
FFMPEG_EXE = r"C:\Software\ffmpeg.exe"
FFPROBE_EXE = r"C:\Software\ffprobe.exe"
OUTPUT_BASE_DIR = r"H:\VOD"
APP_DIR = Path(__file__).resolve().parent
CONFIG_PATH = APP_DIR / "config.json"
DEFAULT_CONFIG = {
"ffmpeg": {
"ffmpeg_exe": "",
"ffprobe_exe": "",
"search_dirs": [
r"C:\Tools\FFMPEG",
r"C:\Software",
],
},
"output": {
"base_dir": "",
"preferred_dirs": [
r"F:\VOD",
r"H:\VOD",
],
},
"video": {
"hd_bitrate": "30M",
"hd_maxrate": "35M",
"hd_bufsize": "50M",
"hd_level": "4.1",
"sd_bitrate": "8M",
"sd_maxrate": "10M",
"sd_bufsize": "15M",
"sd_level": "3.1",
"preset": "slow",
"tune": "film",
},
"audio": {
"mp4_bitrate": "256k",
"sample_rate": "48000",
"pcm_codec": "pcm_s24le",
},
}
def deep_merge_config(default: dict, override: dict) -> dict:
merged = dict(default)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = deep_merge_config(merged[key], value)
else:
merged[key] = value
return merged
def write_default_config(path: Path) -> None:
path.write_text(json.dumps(DEFAULT_CONFIG, indent=2) + "\n", encoding="utf-8")
def load_config() -> dict:
if not CONFIG_PATH.exists():
write_default_config(CONFIG_PATH)
return DEFAULT_CONFIG
try:
user_config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise RuntimeError(f"config.json ist ungueltig: {exc}") from exc
return deep_merge_config(DEFAULT_CONFIG, user_config)
def find_executable(name: str, configured_path: str, search_dirs: list[str]) -> str:
if configured_path:
path = Path(configured_path)
if path.is_file():
return str(path)
raise FileNotFoundError(f"Konfigurierter Pfad fuer {name} existiert nicht: {configured_path}")
from_path = shutil.which(name)
if from_path:
return from_path
for directory in search_dirs:
candidate = Path(directory) / name
if candidate.is_file():
return str(candidate)
searched = ", ".join(["PATH", *search_dirs])
raise FileNotFoundError(f"{name} nicht gefunden. Gesucht in: {searched}")
def choose_output_base_dir(configured_path: str, preferred_dirs: list[str]) -> str:
if configured_path:
return configured_path
for directory in preferred_dirs:
if Path(directory).is_dir():
return directory
return preferred_dirs[0] if preferred_dirs else str(APP_DIR)
CONFIG = load_config()
FFMPEG_EXE = find_executable("ffmpeg.exe", CONFIG["ffmpeg"]["ffmpeg_exe"], CONFIG["ffmpeg"]["search_dirs"])
FFPROBE_EXE = find_executable("ffprobe.exe", CONFIG["ffmpeg"]["ffprobe_exe"], CONFIG["ffmpeg"]["search_dirs"])
OUTPUT_BASE_DIR = choose_output_base_dir(CONFIG["output"]["base_dir"], CONFIG["output"]["preferred_dirs"])
GERMAN_LANGS = {"DEU", "GER"}
LANGUAGE_NAMES = {
@@ -372,16 +466,18 @@ def build_commands(plan: JobPlan) -> list[tuple[str, list[str]]]:
def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
profile = plan.video_profile
selected_audio = plan.selected_mp4_audio
video_config = CONFIG["video"]
audio_config = CONFIG["audio"]
if profile.is_sd:
bv = "8M"
maxr = "10M"
bufs = "15M"
level = "3.1"
bv = video_config["sd_bitrate"]
maxr = video_config["sd_maxrate"]
bufs = video_config["sd_bufsize"]
level = video_config["sd_level"]
else:
bv = "30M"
maxr = "35M"
bufs = "50M"
level = "4.1"
bv = video_config["hd_bitrate"]
maxr = video_config["hd_maxrate"]
bufs = video_config["hd_bufsize"]
level = video_config["hd_level"]
cmd = [
FFMPEG_EXE,
@@ -415,9 +511,9 @@ def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
"-bufsize",
bufs,
"-preset",
"slow",
video_config["preset"],
"-tune",
"film",
video_config["tune"],
"-x264-params",
f"keyint={profile.keyint}:min-keyint=2:scenecut=40:bframes=3:aq-mode=2",
"-color_primaries",
@@ -429,11 +525,11 @@ def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
"-c:a",
"aac",
"-b:a",
"256k",
audio_config["mp4_bitrate"],
"-ac:a:0",
"2",
"-ar:a:0",
"48000",
audio_config["sample_rate"],
"-f",
"mp4",
"-movflags",
@@ -456,12 +552,13 @@ def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
def build_audio_mov_command(plan: JobPlan, roles: list[AudioRole], output_path: str) -> list[str]:
cmd = [FFMPEG_EXE, "-hide_banner", "-y", "-i", plan.input_file]
audio_config = CONFIG["audio"]
for role in roles:
cmd.extend(["-map", f"0:{role.stream_index}"])
cmd.extend(["-vn", "-map_chapters", "-1", "-map_metadata", "-1"])
for idx, role in enumerate(roles):
cmd.extend([f"-c:a:{idx}", "pcm_s24le", f"-ar:a:{idx}", "48000"])
cmd.extend([f"-c:a:{idx}", audio_config["pcm_codec"], f"-ar:a:{idx}", audio_config["sample_rate"]])
cmd.extend([f"-metadata:s:a:{idx}", f"language={role.language.lower()}"])
title = role.display_language
if role.layout_from_name:
@@ -502,6 +599,8 @@ def run_plan(plan: JobPlan, log: Callable[[str], None] = print) -> None:
def log_plan(plan: JobPlan, log: Callable[[str], None] = print) -> None:
profile = plan.video_profile
log(f"Quelle: {plan.input_file}")
log(f"FFmpeg: {FFMPEG_EXE}")
log(f"FFprobe: {FFPROBE_EXE}")
log(f"Video: {profile.width}x{profile.height} @ {profile.fps:.3f} fps, {profile.target_name}")
if profile.needs_conversion and not profile.can_convert_colorspace:
log("Farbraum: Quell-Metadaten unvollstaendig; setze Ziel-Metadaten ohne Colorspace-Filter.")