Add JSON configuration and path discovery
This commit is contained in:
+115
-16
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user