Add JSON configuration and path discovery
This commit is contained in:
@@ -25,9 +25,9 @@
|
|||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Windows (getestet)
|
- 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+
|
- Python 3.10+
|
||||||
- Schreibzugriff auf den Ausgabeordner `H:\VOD`
|
- Schreibzugriff auf den Ausgabeordner `F:\VOD` oder `H:\VOD`
|
||||||
|
|
||||||
## Installation & Nutzung
|
## Installation & Nutzung
|
||||||
|
|
||||||
@@ -129,14 +129,54 @@ Film_forced.srt
|
|||||||
|
|
||||||
## Konfiguration
|
## 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
|
```json
|
||||||
FFMPEG_EXE = r"C:\Software\ffmpeg.exe"
|
{
|
||||||
FFPROBE_EXE = r"C:\Software\ffprobe.exe"
|
"ffmpeg": {
|
||||||
OUTPUT_BASE_DIR = r"H:\VOD"
|
"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
|
## 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.
|
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
@@ -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
@@ -5,6 +5,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -25,9 +26,102 @@ except Exception:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 1. KONFIGURATION UND PFADE
|
# 1. KONFIGURATION UND PFADE
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
FFMPEG_EXE = r"C:\Software\ffmpeg.exe"
|
APP_DIR = Path(__file__).resolve().parent
|
||||||
FFPROBE_EXE = r"C:\Software\ffprobe.exe"
|
CONFIG_PATH = APP_DIR / "config.json"
|
||||||
OUTPUT_BASE_DIR = r"H:\VOD"
|
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"}
|
GERMAN_LANGS = {"DEU", "GER"}
|
||||||
LANGUAGE_NAMES = {
|
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]:
|
def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
|
||||||
profile = plan.video_profile
|
profile = plan.video_profile
|
||||||
selected_audio = plan.selected_mp4_audio
|
selected_audio = plan.selected_mp4_audio
|
||||||
|
video_config = CONFIG["video"]
|
||||||
|
audio_config = CONFIG["audio"]
|
||||||
if profile.is_sd:
|
if profile.is_sd:
|
||||||
bv = "8M"
|
bv = video_config["sd_bitrate"]
|
||||||
maxr = "10M"
|
maxr = video_config["sd_maxrate"]
|
||||||
bufs = "15M"
|
bufs = video_config["sd_bufsize"]
|
||||||
level = "3.1"
|
level = video_config["sd_level"]
|
||||||
else:
|
else:
|
||||||
bv = "30M"
|
bv = video_config["hd_bitrate"]
|
||||||
maxr = "35M"
|
maxr = video_config["hd_maxrate"]
|
||||||
bufs = "50M"
|
bufs = video_config["hd_bufsize"]
|
||||||
level = "4.1"
|
level = video_config["hd_level"]
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
FFMPEG_EXE,
|
FFMPEG_EXE,
|
||||||
@@ -415,9 +511,9 @@ def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
|
|||||||
"-bufsize",
|
"-bufsize",
|
||||||
bufs,
|
bufs,
|
||||||
"-preset",
|
"-preset",
|
||||||
"slow",
|
video_config["preset"],
|
||||||
"-tune",
|
"-tune",
|
||||||
"film",
|
video_config["tune"],
|
||||||
"-x264-params",
|
"-x264-params",
|
||||||
f"keyint={profile.keyint}:min-keyint=2:scenecut=40:bframes=3:aq-mode=2",
|
f"keyint={profile.keyint}:min-keyint=2:scenecut=40:bframes=3:aq-mode=2",
|
||||||
"-color_primaries",
|
"-color_primaries",
|
||||||
@@ -429,11 +525,11 @@ def build_pvd_mp4_command(plan: JobPlan) -> list[str]:
|
|||||||
"-c:a",
|
"-c:a",
|
||||||
"aac",
|
"aac",
|
||||||
"-b:a",
|
"-b:a",
|
||||||
"256k",
|
audio_config["mp4_bitrate"],
|
||||||
"-ac:a:0",
|
"-ac:a:0",
|
||||||
"2",
|
"2",
|
||||||
"-ar:a:0",
|
"-ar:a:0",
|
||||||
"48000",
|
audio_config["sample_rate"],
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
"mp4",
|
||||||
"-movflags",
|
"-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]:
|
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]
|
cmd = [FFMPEG_EXE, "-hide_banner", "-y", "-i", plan.input_file]
|
||||||
|
audio_config = CONFIG["audio"]
|
||||||
for role in roles:
|
for role in roles:
|
||||||
cmd.extend(["-map", f"0:{role.stream_index}"])
|
cmd.extend(["-map", f"0:{role.stream_index}"])
|
||||||
cmd.extend(["-vn", "-map_chapters", "-1", "-map_metadata", "-1"])
|
cmd.extend(["-vn", "-map_chapters", "-1", "-map_metadata", "-1"])
|
||||||
|
|
||||||
for idx, role in enumerate(roles):
|
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()}"])
|
cmd.extend([f"-metadata:s:a:{idx}", f"language={role.language.lower()}"])
|
||||||
title = role.display_language
|
title = role.display_language
|
||||||
if role.layout_from_name:
|
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:
|
def log_plan(plan: JobPlan, log: Callable[[str], None] = print) -> None:
|
||||||
profile = plan.video_profile
|
profile = plan.video_profile
|
||||||
log(f"Quelle: {plan.input_file}")
|
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}")
|
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:
|
if profile.needs_conversion and not profile.can_convert_colorspace:
|
||||||
log("Farbraum: Quell-Metadaten unvollstaendig; setze Ziel-Metadaten ohne Colorspace-Filter.")
|
log("Farbraum: Quell-Metadaten unvollstaendig; setze Ziel-Metadaten ohne Colorspace-Filter.")
|
||||||
|
|||||||
Reference in New Issue
Block a user