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.

Revisions

  1. YUKI2eN3e created this gist Jul 4, 2023.
    168 changes: 168 additions & 0 deletions shrink-video.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,168 @@
    #!/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()