silent and metadata for ffmpeg
This commit is contained in:
1 parent
dbd6dc9e54
commit
57d5cf3d34
2 files changed
+170
-29
No files matched your search
+168
-27
@@ -18,8 +18,8 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
|
|
||||||
from serverlibary import safe_rename,safe_delete
|
from serverlibary import safe_rename
|
||||||
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm",".mp3"}
|
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm",".mp3", ".m4a", ".m4b", ".opus"}
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# OUTPUT ENFORCEMENT (clean + centralized)
|
# OUTPUT ENFORCEMENT (clean + centralized)
|
||||||
@@ -36,6 +36,7 @@ REQUIRES_OUTPUT = {
|
|||||||
"filter-audio",
|
"filter-audio",
|
||||||
"cut",
|
"cut",
|
||||||
"set-primary-audio",
|
"set-primary-audio",
|
||||||
|
"metaset",
|
||||||
}
|
}
|
||||||
# ---------- Utilities ---------- #
|
# ---------- Utilities ---------- #
|
||||||
|
|
||||||
@@ -51,20 +52,64 @@ def ensure(value, prompt):
|
|||||||
return ask(prompt)
|
return ask(prompt)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def run_ffmpeg(stream, overwrite=True):
|
def move_override_file(input_path, backup_files=False,silent=False):
|
||||||
"""
|
if input_path.is_file():
|
||||||
Run FFmpeg and stream output directly to terminal.
|
input_path=input_path.parent
|
||||||
|
for override_path in input_path.glob("*_override.*"):
|
||||||
|
if not override_path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
- No log buffering
|
stem = override_path.stem
|
||||||
- Real-time progress/errors visible
|
suffix = override_path.suffix
|
||||||
- Easier debugging than capture_stderr=True
|
|
||||||
"""
|
if not stem.endswith("_override"):
|
||||||
try:
|
continue
|
||||||
ffmpeg.run(
|
|
||||||
stream,
|
base = stem.removesuffix("_override")
|
||||||
overwrite_output=overwrite
|
|
||||||
|
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:
|
except ffmpeg.Error as e:
|
||||||
|
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")
|
print("FFmpeg failed.\n")
|
||||||
if e.stderr:
|
if e.stderr:
|
||||||
print(e.stderr.decode(errors="ignore"))
|
print(e.stderr.decode(errors="ignore"))
|
||||||
@@ -623,6 +668,67 @@ def cmd_set_primary_audio(args):
|
|||||||
|
|
||||||
return out
|
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):
|
def cmd_apply_chapters(args):
|
||||||
"""
|
"""
|
||||||
Apply chapters to an M4B using a simple title-only text file.
|
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 = argparse.ArgumentParser(add_help=False)
|
||||||
base_p.add_argument("input", nargs="?", help="Path to input media file")
|
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("-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
|
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 = sub.add_parser("test", aliases=["t"], help="Run test function")
|
||||||
p_test.set_defaults(func=cmd_test, command="test")
|
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
|
# CONVERT AV
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -1109,6 +1211,30 @@ def main():
|
|||||||
p_pa.add_argument("-a", "--audio-index", type=int)
|
p_pa.add_argument("-a", "--audio-index", type=int)
|
||||||
p_pa.set_defaults(func=cmd_set_primary_audio, command="set-primary-audio")
|
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
|
# PARSE
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@@ -1145,15 +1271,19 @@ def main():
|
|||||||
if not Path(args.input).exists():
|
if not Path(args.input).exists():
|
||||||
parser.error("Input file does not exist!")
|
parser.error("Input file does not exist!")
|
||||||
if args.command in REQUIRES_OUTPUT and not getattr(args, "output", None):
|
if args.command in REQUIRES_OUTPUT and not getattr(args, "output", None):
|
||||||
|
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)")
|
args.output = ensure(getattr(args, "output", None), "Output file (enter for default file)")
|
||||||
if args.output == "" or args.output is None:
|
if args.output == "" or args.output is None:
|
||||||
input_path = Path(args.input)
|
input_path = Path(args.input)
|
||||||
args.output = str(input_path.with_name(f"{input_path.stem}_{args.command}{input_path.suffix}"))
|
args.output = str(input_path.with_name(f"{input_path.stem}_{args.command}{input_path.suffix}"))
|
||||||
|
print(args.output)
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# COMMAND-SPECIFIC REQUIRED FIELDS
|
# 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":
|
if args.command == "convert-av":
|
||||||
args.preset = ensure(getattr(args, "preset", None),
|
args.preset = ensure(getattr(args, "preset", None),
|
||||||
"Encoding speed/quality preset (default: medium, e.g. ultrafast, fast, slow, veryslow)")
|
"Encoding speed/quality preset (default: medium, e.g. ultrafast, fast, slow, veryslow)")
|
||||||
@@ -1180,6 +1310,12 @@ def main():
|
|||||||
if args.audio_index is None:
|
if args.audio_index is None:
|
||||||
args.audio_index = 1
|
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
|
# EXECUTE COMMAND
|
||||||
@@ -1198,8 +1334,11 @@ def main():
|
|||||||
if file.suffix.lower() not in VIDEO_EXTS:
|
if file.suffix.lower() not in VIDEO_EXTS:
|
||||||
continue
|
continue
|
||||||
print(f"Processing File: {file}")
|
print(f"Processing File: {file}")
|
||||||
out_path = file.with_name(f"{file.stem}_{args.command}{file.suffix}")
|
|
||||||
args.input = str(file)
|
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.output = str(out_path)
|
||||||
args.file_counter = counter
|
args.file_counter = counter
|
||||||
outputs.append(args.func(args))
|
outputs.append(args.func(args))
|
||||||
@@ -1217,11 +1356,13 @@ def main():
|
|||||||
outputs = [outputs]
|
outputs = [outputs]
|
||||||
|
|
||||||
if args.command != "test" and outputs is not None:
|
if args.command != "test" and outputs is not None:
|
||||||
ffmpeg.run(
|
run_ffmpeg(
|
||||||
ffmpeg.merge_outputs(*outputs),
|
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:
|
except ffmpeg.Error as e:
|
||||||
print("FFmpeg failed.\n")
|
print("FFmpeg failed.\n")
|
||||||
if e.stderr:
|
if e.stderr:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def safe_delete(path: Path):
|
|||||||
else:
|
else:
|
||||||
path.unlink()
|
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.
|
Safely rename a file or directory without overwriting existing files.
|
||||||
Automatically appends _1, _2, ... if needed.
|
Automatically appends _1, _2, ... if needed.
|
||||||
@@ -35,7 +35,7 @@ def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False):
|
|||||||
while final_dst.exists():
|
while final_dst.exists():
|
||||||
final_dst = directory / f"{base}_{counter}{ext}"
|
final_dst = directory / f"{base}_{counter}{ext}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
if not silent:
|
||||||
print(f"{src} → {dst}")
|
print(f"{src} → {dst}")
|
||||||
|
|
||||||
if dry:
|
if dry:
|
||||||
|
|||||||
Reference in new issue
Block a user