Skip to content

Instantly share code, notes, and snippets.

@YUKI2eN3e
Created July 4, 2023 05:05
Show Gist options
  • Select an option

  • Save YUKI2eN3e/9c06d5ad28551f92c4eebbfcb1d41c75 to your computer and use it in GitHub Desktop.

Select an option

Save YUKI2eN3e/9c06d5ad28551f92c4eebbfcb1d41c75 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3.10
""" shrink-video.py
Required:
ffmpeg (with libx265 and libfdk_aac)
set-thumbnail (a script that takes the video_filename and the thumbnail_filename in that order)
mediainfo
recycle (command that sends file to the Recycle Bin)
Notes:
Only tested on windows.
Input video is assumed to be mkv and it is assumed that a webp thumbnail is present in the same folder.
"""
import argparse
import subprocess as sp
from dataclasses import dataclass
from os.path import basename, dirname, exists, relpath
from shlex import join as cmd_join
from shlex import split as cmd_split
from sys import platform, stderr
@dataclass
class CliArgs:
file_name: str
work_dir: str | None
output_dir: str
cleanup: bool
def get_args() -> CliArgs:
parser = argparse.ArgumentParser(basename(__file__))
parser.add_argument(
"-f", "--file-name", help="the name of the file to shrink", required=True
)
parser.add_argument(
"-w",
"--work-dir",
help="an intermediate folder to use while converting",
default=None,
)
parser.add_argument(
"-o", "--output-dir", help="where to save the output file", default="."
)
parser.add_argument(
"-c",
"--cleanup",
action="store_true",
help="delete original file and png",
default=False,
)
return CliArgs(**vars(parser.parse_args()))
def _fix_path(path: str) -> str:
if platform == "win32":
path.replace("/", "\\")
if path[-1] != "\\":
path = f"{path}\\"
elif path[-1] != "/":
path = f"{path}/"
return path
def _get_duration(video_filename: str) -> str:
"""Get fps of video using mediainfo.
Args:
video_filename (str): The video to get the fps of.
Returns:
float: The fps of the video.
"""
cmd = f'mediainfo "{video_filename}" | grep "Duration.*:.*$"'
proc = sp.run(cmd, shell=True, capture_output=True)
return " ".join(
c
for c in proc.stdout.splitlines()[0].decode().split()
if c.isdigit() or c in ["h", "min"]
).strip()
def get_base_name(file_name) -> str:
return (
file_name.split(".")[0]
if len(file_name.split(".")) == 2
else ".".join(file_name.split(".")[0:-1])
)
def build_cmd(
base_filename: str,
output_dir: str,
work_dir: str | None = None,
cleanup: bool = False,
) -> str:
cmd = ""
if work_dir is not None and work_dir != output_dir:
work_dir = _fix_path(work_dir)
if cleanup:
cmd = f'''ffmpeg -i "{base_filename}.mkv" -c:v libx265 -vf fps=24 -c:a libfdk_aac "{work_dir}{base_filename}.mp4" & ffmpeg -i "{base_filename}.webp" "{work_dir}{base_filename}.png" & set-thumbnail "{work_dir}{base_filename}.mp4" "{work_dir}{base_filename}.png" & recycle "{base_filename}.mkv" & recycle "{work_dir}{base_filename}.png" & move "{work_dir}{base_filename}.mp4" "{_fix_path(output_dir)}{relpath(base_filename, ".")}.mp4"'''
else:
cmd = f'''ffmpeg -i "{base_filename}.mkv" -c:v libx265 -vf fps=24 -c:a libfdk_aac "{work_dir}{base_filename}.mp4" & ffmpeg -i "{base_filename}.webp" "{work_dir}{base_filename}.png" & set-thumbnail "{work_dir}{base_filename}.mp4" "{work_dir}{base_filename}.png" & move "{work_dir}{base_filename}.mp4" "{_fix_path(output_dir)}{relpath(base_filename, ".")}.mp4"'''
else:
if cleanup:
cmd = f'''ffmpeg -i "{base_filename}.mkv" -c:v libx265 -vf fps=24 -c:a libfdk_aac "{base_filename}.mp4" & ffmpeg -i "{base_filename}.webp" "{base_filename}.png" & set-thumbnail "{base_filename}.mp4" "{base_filename}.png" & recycle "{base_filename}.mkv" & recycle "{base_filename}.png"'''
else:
cmd = f"""ffmpeg -i "{base_filename}.mkv" -c:v libx265 -vf fps=24 -c:a libfdk_aac "{work_dir}{base_filename}.mp4" & ffmpeg -i "{base_filename}.webp" "{work_dir}{base_filename}.png" & set-thumbnail "{work_dir}{base_filename}.mp4" "{work_dir}{base_filename}.png" & """
if dirname(output_dir) != dirname(base_filename):
cmd = f'''{cmd} & move "{base_filename}.mp4" "{_fix_path(output_dir)}{relpath(base_filename, ".")}.mp4"'''
return cmd
def build_cleanup(
cleanup: bool,
converted_filename: str,
source_filename: str,
output_dir: str,
) -> str | bool:
if cleanup:
if (
_get_duration(
f'"{_fix_path(output_dir)}{relpath(converted_filename, ".")}"'
)
== _get_duration()
):
return f'''recycle "{source_filename}" & recycle "{get_base_name(source_filename)}.png"'''
return False
def sanitize_cmd(cmd: str) -> str:
return cmd_join(cmd_split(cmd)).replace("'", '"').replace('"&"', "&")
def run():
args = get_args()
base_filename = get_base_name(args.file_name)
cmd = build_cmd(
base_filename=base_filename,
output_dir=args.output_dir,
work_dir=args.work_dir,
cleanup=args.cleanup,
)
# Convert
cmd = sanitize_cmd(cmd)
print(cmd)
sp.run(cmd, shell=True)
# Cleanup
if args.cleanup:
converted_filename = (
f'"{_fix_path(args.output_dir)}{relpath(base_filename, ".")}.mp4"'
)
if exists(converted_filename):
cleanup_cmd = build_cleanup(
cleanup=args.cleanup,
converted_filename=converted_filename,
source_filename=args.file_name,
output_dir=args.output_dir,
)
if type(cleanup_cmd) == bool and cleanup_cmd == False:
print("Files do not match, cleanup skipped.", file=stderr)
else:
sp.run(sanitize_cmd(cleanup_cmd), shell=True)
if __name__ == "__main__":
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment