silent and metadata for ffmpeg

This commit is contained in:
Lucas Vivohost committed 2026-09-14 09:20:47 +02:00
1 parent dbd6dc9e54
commit 57d5cf3d34
2 files changed
+175 -34

No files matched your search

+172 -31
View File
@@ -18,8 +18,8 @@ import tempfile
from pathlib import Path
from mutagen.easyid3 import EasyID3
from serverlibary import safe_rename,safe_delete
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm",".mp3"}
from serverlibary import safe_rename
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm",".mp3", ".m4a", ".m4b", ".opus"}
# ----------------------------
# OUTPUT ENFORCEMENT (clean + centralized)
@@ -36,6 +36,7 @@ REQUIRES_OUTPUT = {
"filter-audio",
"cut",
"set-primary-audio",
"metaset",
}
# ---------- Utilities ---------- #
@@ -51,23 +52,67 @@ def ensure(value, prompt):
return ask(prompt)
return value
def run_ffmpeg(stream, overwrite=True):
"""
Run FFmpeg and stream output directly to terminal.
def move_override_file(input_path, backup_files=False,silent=False):
if input_path.is_file():
input_path=input_path.parent
for override_path in input_path.glob("*_override.*"):
if not override_path.is_file():
continue
- No log buffering
- Real-time progress/errors visible
- Easier debugging than capture_stderr=True
"""
try:
ffmpeg.run(
stream,
overwrite_output=overwrite
stem = override_path.stem
suffix = override_path.suffix
if not stem.endswith("_override"):
continue
base = stem.removesuffix("_override")
original = input_path / f"{base}{suffix}"
backup = input_path / f".backup_{base}{suffix}"
new_name = input_path / f"{base}{suffix}"
if not original.exists():
print(f"Skip (missing original): {original}")
continue
if backup_files:
# move original -> backup
safe_rename(original, backup,silent=silent)
# move override -> original name
safe_rename(override_path, new_name,silent=silent)
LOG_FILE = Path.home() / "ffmpeg_log.log"
def run_ffmpeg(stream, overwrite=True, loud=False):
kwargs = {
"overwrite_output": overwrite,
}
if loud:
kwargs.update(
capture_stdout=False,
capture_stderr=True,
quiet=True,
)
try:
out, err = ffmpeg.run(stream, **kwargs)
if loud and err:
with open(LOG_FILE, "ab") as log:
log.write(err)
except ffmpeg.Error as e:
print("FFmpeg failed.\n")
if e.stderr:
print(e.stderr.decode(errors="ignore"))
if loud:
with open(LOG_FILE, "ab") as log:
log.write(e.stderr or b"")
print(f"FFmpeg failed. See log: {LOG_FILE}")
else:
print("FFmpeg failed.\n")
if e.stderr:
print(e.stderr.decode(errors="ignore"))
sys.exit(1)
def probe_audio_streams(file):
@@ -623,6 +668,67 @@ def cmd_set_primary_audio(args):
return out
def cmd_show_metadata(args):
"""
Prints metadata directly from ffprobe output.
"""
probe = ffmpeg.probe(args.input)
print("=== FORMAT METADATA ===")
format_tags = probe.get("format", {}).get("tags", {})
for k, v in format_tags.items():
print(f"{k}: {v}")
#print("\n=== STREAM METADATA ===")
#for i, stream in enumerate(probe.get("streams", [])):
# print(f"\n--- Stream {i} ({stream.get('codec_type')}) ---")
# tags = stream.get("tags", {})
# if not tags:
# print("No tags")
# else:
# for k, v in tags.items():
# print(f"{k}: {v}")
return
def cmd_set_metadata(args):
"""
Set metadata for media files.
- Default: uses ffmpeg (stream copy)
- If file is .opus: uses kid3-cli (Vorbis tags)
"""
ext = os.path.splitext(args.input)[1].lower()
# ─────────────────────────────────────────────
# OPUS → use Kid3 CLI (Vorbis comments)
# ─────────────────────────────────────────────
if ext == ".opus" or args.kidflag:
cmd = [
"kid3-cli",
"-c", f"set {args.key} '{args.value}'",
args.input
]
subprocess.run(cmd, check=True)
return None # Kid3 writes in-place
# ─────────────────────────────────────────────
# Default → ffmpeg (copy stream)
# ─────────────────────────────────────────────
inp = ffmpeg.input(args.input)
out = ffmpeg.output(
inp,
args.output,
c="copy",
metadata=f"{args.key}={args.value}"
)
return out
def cmd_apply_chapters(args):
"""
Apply chapters to an M4B using a simple title-only text file.
@@ -1010,7 +1116,10 @@ def main():
base_p = argparse.ArgumentParser(add_help=False)
base_p.add_argument("input", nargs="?", help="Path to input media file")
base_p.add_argument("-o", "--output", help="Path for output file")
base_p.add_argument("--override", action="store_true", help="Override input")
base_p.add_argument("--backup", action="store_true", help="Backup original file when overriding")
base_p.add_argument("--loud", action="store_false", help="Run ffmpeg loud")
base_p.add_argument("--kidflag", action="store_true", help="Use kid3 instead of ffmpeg")
sub = parser.add_subparsers(dest="command") # command is optional now
# ----------------------------
@@ -1023,13 +1132,6 @@ def main():
p_test = sub.add_parser("test", aliases=["t"], help="Run test function")
p_test.set_defaults(func=cmd_test, command="test")
# ----------------------------
# CHAPTERS
# ----------------------------
p_ch = sub.add_parser("chapters",aliases=["ch"],parents=[base_p],help="Apply chapter metadata from ffmetadata file")
p_ch.add_argument("-cf","--chapter-file",help="Path to ffmetadata chapters file (e.g. chapters.txt)")
p_ch.set_defaults(func=cmd_apply_chapters, command="chapters")
# ----------------------------
# CONVERT AV
# ----------------------------
@@ -1109,6 +1211,30 @@ def main():
p_pa.add_argument("-a", "--audio-index", type=int)
p_pa.set_defaults(func=cmd_set_primary_audio, command="set-primary-audio")
# ----------------------------
# CHAPTERS
# ----------------------------
p_ch = sub.add_parser("chapters",aliases=["ch"],parents=[base_p],help="Apply chapter metadata from ffmetadata file")
p_ch.add_argument("-cf","--chapter-file",help="Path to ffmetadata chapters file (e.g. chapters.txt)")
p_ch.set_defaults(func=cmd_apply_chapters, command="chapters")
# ----------------------------
# METADATA
# ----------------------------
# PRINT
p_ch = sub.add_parser("metaprint", aliases=["mp"], parents=[base_p],
help="Show metadata")
p_ch.set_defaults(func=cmd_show_metadata, command="metaprint")
# SET
p_ch = sub.add_parser("metaset",aliases=["ms"],parents=[base_p],help="Apply metadata key and value to input file")
p_ch.add_argument("-k","--key",help="Metadata key name)")
p_ch.add_argument("-v","--value",help="Metadata key value")
p_ch.set_defaults(func=cmd_set_metadata, command="metaset")
# =========================================================
# PARSE
# =========================================================
@@ -1145,15 +1271,19 @@ def main():
if not Path(args.input).exists():
parser.error("Input file does not exist!")
if args.command in REQUIRES_OUTPUT and not getattr(args, "output", None):
args.output = ensure(getattr(args, "output", None), "Output file (enter for default file)")
input_path = Path(args.input)
if args.override:
args.output = str(input_path.with_name(f"{input_path.stem}_override{input_path.suffix}"))
else:
args.output = ensure(getattr(args, "output", None), "Output file (enter for default file)")
if args.output == "" or args.output is None:
input_path = Path(args.input)
args.output = str(input_path.with_name(f"{input_path.stem}_{args.command}{input_path.suffix}"))
print(args.output)
# ----------------------------
# COMMAND-SPECIFIC REQUIRED FIELDS
# ----------------------------
if args.command == "chapters":
args.chapter_file = ensure(getattr(args, "chapter_file", None),"File with chapternames")
if args.command == "convert-av":
args.preset = ensure(getattr(args, "preset", None),
"Encoding speed/quality preset (default: medium, e.g. ultrafast, fast, slow, veryslow)")
@@ -1180,6 +1310,12 @@ def main():
if args.audio_index is None:
args.audio_index = 1
if args.command == "chapters":
args.chapter_file = ensure(getattr(args, "chapter_file", None), "File with chapternames")
if args.command == "metaset":
args.chapter_file = ensure(getattr(args, "key", None), "Key to change")
args.chapter_file = ensure(getattr(args, "value", None), "Key value")
# ----------------------------
# EXECUTE COMMAND
@@ -1198,8 +1334,11 @@ def main():
if file.suffix.lower() not in VIDEO_EXTS:
continue
print(f"Processing File: {file}")
out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}")
args.input = str(file)
if args.override:
out_path = file.with_name(f"{file.stem}_override{file.suffix}")
else:
out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}")
args.output = str(out_path)
args.file_counter = counter
outputs.append(args.func(args))
@@ -1217,11 +1356,13 @@ def main():
outputs = [outputs]
if args.command != "test" and outputs is not None:
ffmpeg.run(
run_ffmpeg(
ffmpeg.merge_outputs(*outputs),
overwrite_output=True
overwrite=True,
loud=args.loud
)
if args.override:
move_override_file(input_path, args.backup,args.loud)
except ffmpeg.Error as e:
print("FFmpeg failed.\n")
if e.stderr:
+3 -3
View File
@@ -11,7 +11,7 @@ def safe_delete(path: Path):
else:
path.unlink()
def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False):
def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False, silent=False):
"""
Safely rename a file or directory without overwriting existing files.
Automatically appends _1, _2, ... if needed.
@@ -35,8 +35,8 @@ def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False):
while final_dst.exists():
final_dst = directory / f"{base}_{counter}{ext}"
counter += 1
print(f"{src} → {dst}")
if not silent:
print(f"{src} → {dst}")
if dry:
return