From 6da00cd98b3bb270516b97af81b6f8461ff5704e Mon Sep 17 00:00:00 2001 From: VineFeeder Date: Mon, 22 Dec 2025 10:36:27 +0000 Subject: [PATCH] new feature --- .../vinefeeder/src/vinefeeder/config.yaml | 2 +- post_processing/extract_mks_subs.py | 172 ++++++++++++++++++ post_processing/mkv_to_mp4.py | 162 +++++++++++++++++ post_processing/post_processing.md | 37 ++++ 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 post_processing/extract_mks_subs.py create mode 100644 post_processing/mkv_to_mp4.py create mode 100644 post_processing/post_processing.md diff --git a/packages/vinefeeder/src/vinefeeder/config.yaml b/packages/vinefeeder/src/vinefeeder/config.yaml index d4871c3..9e2397e 100644 --- a/packages/vinefeeder/src/vinefeeder/config.yaml +++ b/packages/vinefeeder/src/vinefeeder/config.yaml @@ -1,3 +1,3 @@ -BATCH_DOWNLOAD: false +BATCH_DOWNLOAD: true TERMINAL: gnome-terminal TERMINAL_RESET: true diff --git a/post_processing/extract_mks_subs.py b/post_processing/extract_mks_subs.py new file mode 100644 index 0000000..3d1a0e3 --- /dev/null +++ b/post_processing/extract_mks_subs.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Selecting -S (subtitles only) as a download option results in an mks file +which needs convertion to something acceptable for adding to a video playback. +This is a post processing routne that operated in teh root foler of any number of +mks files. +The sceipt will recursively extract subtitle tracks from .mks files using mkvextract. + +A CLI for example would mkvextract "Taggart S01E02.mks" tracks 0:tS01E02.srt but the script +finds each title and run the CLI on it. + + +Default behavior: +- Find all *.mks under a given root +- Extract track 0 to an .srt with the same base name, in the same folder + e.g. "Taggart S01E02.mks" -> "Taggart S01E02.srt" + +Requires: +- mkvextract (part of MKVToolNix) available on PATH +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Extract SRT subtitles from .mks files (recursively) using mkvextract." + ) + p.add_argument( + "root", + nargs="?", + default=".", + help="Root directory to scan (default: current directory).", + ) + p.add_argument( + "--track", + type=int, + default=0, + help="Track index to extract (default: 0).", + ) + p.add_argument( + "--ext", + default=".mks", + help="Input extension to scan for (default: .mks).", + ) + p.add_argument( + "--out-ext", + default=".srt", + help="Output extension to write (default: .srt).", + ) + p.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing output files.", + ) + p.add_argument( + "--dry-run", + action="store_true", + help="Print what would be done, but don't run mkvextract.", + ) + p.add_argument( + "--verbose", + action="store_true", + help="Print mkvextract output for each file.", + ) + return p.parse_args() + + +def run_extract( + mkvextract_path: str, + in_file: Path, + out_file: Path, + track: int, + overwrite: bool, + dry_run: bool, + verbose: bool, +) -> bool: + if out_file.exists() and not overwrite: + print(f"SKIP (exists): {out_file}") + return True + + cmd = [ + mkvextract_path, + str(in_file), + "tracks", + f"{track}:{out_file}", + ] + + if dry_run: + print("DRY:", " ".join(map(str, cmd))) + return True + + # Ensure parent exists (it should, but just in case you change output logic later) + out_file.parent.mkdir(parents=True, exist_ok=True) + + try: + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + except FileNotFoundError: + print("ERROR: mkvextract not found (is MKVToolNix installed and on PATH?)", file=sys.stderr) + return False + + if verbose and proc.stdout: + print(proc.stdout.rstrip()) + + if proc.returncode == 0 and out_file.exists(): + print(f"OK : {in_file.name} -> {out_file.name}") + return True + + print(f"FAIL: {in_file}", file=sys.stderr) + if proc.stdout: + print(proc.stdout.rstrip(), file=sys.stderr) + return False + + +def main() -> int: + args = parse_args() + + mkvextract_path = shutil.which("mkvextract") + if not mkvextract_path: + print("ERROR: mkvextract not found on PATH. Install MKVToolNix.", file=sys.stderr) + return 2 + + root = Path(args.root).expanduser().resolve() + if not root.exists(): + print(f"ERROR: Root path does not exist: {root}", file=sys.stderr) + return 2 + + in_ext = args.ext if args.ext.startswith(".") else f".{args.ext}" + out_ext = args.out_ext if args.out_ext.startswith(".") else f".{args.out_ext}" + + files = sorted(p for p in root.rglob(f"*{in_ext}") if p.is_file()) + if not files: + print(f"No {in_ext} files found under {root}") + return 0 + + ok = 0 + fail = 0 + + for in_file in files: + out_file = in_file.with_suffix(out_ext) + success = run_extract( + mkvextract_path=mkvextract_path, + in_file=in_file, + out_file=out_file, + track=args.track, + overwrite=args.overwrite, + dry_run=args.dry_run, + verbose=args.verbose, + ) + if success: + ok += 1 + else: + fail += 1 + + print(f"\nDone. OK={ok}, FAIL={fail}, TOTAL={ok+fail}") + return 0 if fail == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/post_processing/mkv_to_mp4.py b/post_processing/mkv_to_mp4.py new file mode 100644 index 0000000..a7b010a --- /dev/null +++ b/post_processing/mkv_to_mp4.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Recursively convert form .mkv files to mp4 using ffmpeg. + +Default behavior: +- Find all *.mkv under a given root +- Convert to mp4 with the same base name, in the same folder + e.g. "Taggart S01E02.mkv" -> "Taggart S01E02.mp4" + +Requires: +- ffmpeg available on PATH +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Convert mkv files to mp4 (recursively) using ffmpeg." + ) + p.add_argument( + "root", + nargs="?", + default=".", + help="Root directory to scan (default: current directory).", + ) + p.add_argument( + "--ext", + default=".mkv", + help="Input extension to scan for (default: .mkv).", + ) + p.add_argument( + "--out-ext", + default=".mp4", + help="Output extension to write (default: .mp4).", + ) + p.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing output files.", + ) + p.add_argument( + "--dry-run", + action="store_true", + help="Print what would be done, but don't run ffmpeg.", + ) + p.add_argument( + "--verbose", + action="store_true", + help="Print ffmpeg output for each file.", + ) + return p.parse_args() + + +def run_convert( + ffmpeg_path: str, + in_file: Path, + out_file: str, + overwrite: bool, + dry_run: bool, + verbose: bool, + ) -> bool: + + out_file = Path(out_file) + in_file = Path(in_file) + + if out_file.exists() and not overwrite: + print(f"SKIP (exists): {out_file}") + return True + + cmd = [ + ffmpeg_path, + "-i", + in_file, + "-c", + "copy", + out_file, + ] + + if dry_run: + print("DRY:", " ".join(map(str, cmd))) + return True + + # Ensure parent exists (it should, but just in case you change output logic later) + out_file.parent.mkdir(parents=True, exist_ok=True) + + try: + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + except FileNotFoundError: + print("ERROR: ffmpeg not found (is MKVToolNix installed and on PATH?)", file=sys.stderr) + return False + + if verbose and proc.stdout: + print(proc.stdout.rstrip()) + + if proc.returncode == 0 and out_file.exists(): + print(f"OK : {in_file.name} -> {out_file.name}") + return True + + print(f"FAIL: {in_file}", file=sys.stderr) + if proc.stdout: + print(proc.stdout.rstrip(), file=sys.stderr) + return False + + +def main() -> int: + args = parse_args() + + ffmpeg_path = shutil.which("ffmpeg") + if not ffmpeg_path: + print("ERROR: ffmpeg not found on PATH. Install MKVToolNix.", file=sys.stderr) + return 2 + + root = Path(args.root).expanduser().resolve() + if not root.exists(): + print(f"ERROR: Root path does not exist: {root}", file=sys.stderr) + return 2 + + in_ext = args.ext if args.ext.startswith(".") else f".{args.ext}" + out_ext = args.out_ext if args.out_ext.startswith(".") else f".{args.out_ext}" + + files = sorted(p for p in root.rglob(f"*{in_ext}") if p.is_file()) + if not files: + print(f"No {in_ext} files found under {root}") + return 0 + + ok = 0 + fail = 0 + + for in_file in files: + out_file = in_file.with_suffix(out_ext) + success = run_convert( + ffmpeg_path=ffmpeg_path, + in_file=in_file, + out_file=out_file, + overwrite=args.overwrite, + dry_run=args.dry_run, + verbose=args.verbose, + ) + if success: + ok += 1 + else: + fail += 1 + + print(f"\nDone. OK={ok}, FAIL={fail}, TOTAL={ok+fail}") + return 0 if fail == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/post_processing/post_processing.md b/post_processing/post_processing.md new file mode 100644 index 0000000..8e507d8 --- /dev/null +++ b/post_processing/post_processing.md @@ -0,0 +1,37 @@ +# Post Processing. + +With a mega program like envied it is sometimes difficult to make internal program changes to reflect personal needs. + +Using a post-processor on the downloaded results may provide a solution. + +I have found two unmet needs: + +* Extract srt subtitles +* Convert mkv output to mp4 (or any other video container format, with code adjustment) + +Here are two scripts which operate from a root folder (downloads - for instance), and operate on all the mkv files found within. The original files are left in place - for you to remove as necessary. + +## Extract srt subtitles + +Use +* uv run envied dl -S my5 https://www.channel5.com/show/taggart/season-1/killer + +-S tells envied to download subtitles only. However it produces an mks file as a result. + +Run 'python extract_mks_subs.py' in the root folder with your mks downloaded files. There are options: --dry-run will allow checking all is well before extraction. + +If you start with a full mkv container - video, audio and subtitles tracks - and wish to extract subtitles, then they will no longer be held in track 0 of an mks file - the program defaults. So use the --track parameter and set it to the third track of 0,1,2. And the full container extension is mkv -the script needs the default settings over-riding with: + + * 'python extract_mks_subs.py --track 2 --ext mkv '. + + +## Convert to mp4 + +Run +* 'python mkv_to_mp4.py' + +in the root folder with mkv files. + +The option: --dry-run will allow checking all is well before converion. + +Conversion to other formats is complex and not suited to this simple routine as the audio/video codecs would each need need re-coding to suit the required output. \ No newline at end of file