silent and metadata for ffmpeg
This commit is contained in:
1 parent
dbd6dc9e54
commit
57d5cf3d34
2 files changed
+175
-34
No files matched your search
+172
-31
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user