~warmwaffles/gopro-utils

9821dda1cfa92bc0de6cab2131e353f23c0623cf — Matthew Johnston 7 months ago b5b599e
Adds gopro python file
1 files changed, 296 insertions(+), 0 deletions(-)

A gopro.py
A gopro.py => gopro.py +296 -0
@@ 0,0 1,296 @@
import argparse
import re
import subprocess
from pathlib import Path


FORMATS = {
    "hero7": {
        "timelapse": re.compile(r"(G(\d{3})(\d{4}))\.(\w{3})"),
        "video": re.compile(r"(GH(\d{2})(\d{4}))\.(\w{3})"),
        "lowres": re.compile(r"(GL(\d{2})(\d{4}))\.(\w{3})"),
    },
}

def parse_video_name(model, format, path):
    regex = FORMATS[model][format]
    match = regex.match(path.name)
    if not match:
        return None

    return {
        "filename": match.group(1),
        "chapter": match.group(2),
        "number": match.group(3),
        "ext": match.group(4),
    }


def parse_timelapse_name(model, path):
    regex = FORMATS[model]["timelapse"]
    match = regex.match(path.name)
    if not match:
        return None

    return {
        "group_number": match.group(2),
        "file_number": match.group(3),
        "ext": match.group(4),
    }


def str_to_bool(v):
    if isinstance(v, bool):
        return v
    if v.lower() in ("yes", "true", "t", "y", "1"):
        return True
    elif v.lower() in ("no", "false", "f", "n", "0"):
        return False
    else:
        raise argparse.ArgumentTypeError("Boolean value expected.")


def organize(args):
    video_dir = Path(args.video_dir).resolve()
    hi_res_dir = Path(video_dir, "hi-res").resolve()
    low_res_dir = Path(video_dir, "low-res").resolve()
    thumb_dir = Path(video_dir, "thumbnails").resolve()
    timelapse_dir = Path(args.timelapse_dir).resolve()

    print(f"model:         {args.model}")
    print(f"video dir:     {video_dir}")
    print(f"timelapse dir: {timelapse_dir}")

    if not FORMATS.get(args.model):
        sys.exit(f"Unsupported go pro model \"{model}\"")

    video_dir.mkdir(exist_ok=True)
    hi_res_dir.mkdir(exist_ok=True)
    low_res_dir.mkdir(exist_ok=True)
    thumb_dir.mkdir(exist_ok=True)
    timelapse_dir.mkdir(exist_ok=True)

    directories = [Path(d).resolve() for d in args.directory]
    print("Organizing the following directories")
    for directory in directories:
        print(directory)

    for directory in directories:
        for path in directory.glob("*.MP4"):
            match = parse_video_name(args.model, "video", path)
            if match:
                new_path = Path(
                    hi_res_dir, f"{match['number']} {match['chapter']}.{match['ext']}"
                )
                print(f"RENAME {path} => {new_path}")
                new_path.parent.mkdir(parents=True, exist_ok=True)
                path.rename(new_path)

        for path in directory.glob("*.LRV"):
            match = parse_video_name(args.model, "lowres", path)
            if match:
                new_path = Path(
                    low_res_dir,
                    f"{match['number']} {match['chapter']}.{match['ext']}"
                )
                print(f"RENAME {path} => {new_path}")
                new_path.parent.mkdir(parents=True, exist_ok=True)
                path.rename(new_path)

        for path in directory.glob("*.THM"):
            match = parse_video_name(args.model, "video", path)
            if match:
                new_path = Path(
                    thumb_dir,
                    f"{match['number']} {match['chapter']}.{match['ext']}"
                )
                print(f"RENAME {path} => {new_path}")
                new_path.parent.mkdir(parents=True, exist_ok=True)
                path.rename(new_path)

        for path in directory.glob("*.JPG"):
            match = parse_timelapse_name(args.model, path)
            if not match:
                continue

            group_dir = Path(timelapse_dir, match['group_number'])
            new_path = Path(group_dir, f"{match['file_number']}.{match['ext']}")
            print(f"RENAME {path} => {new_path}")
            group_dir.mkdir(exist_ok=True)
            path.rename(Path(group_dir, path.name))

        # Delete all empty directories
        if len([f for f in directory.iterdir()]) == 0:
            print(f"DELETE {directory}")
            directory.rmdir()

    if args.archive:
        for path in list(timelapse_dir.glob("*")):
            if path.is_dir():
                parent_path = path.parent
                outfile = Path(timelapse_dir, f"{path.name}.tar.zst")
                print(f"Archiving {path} => {outfile}")
                subprocess.run(
                    [
                        "tar",
                        "--directory",
                        parent_path,
                        "--zstd",
                        "-cf",
                        outfile,
                        path.name
                    ],
                    check=True,
                )



def timelapse(args):
    working_dir = Path(args.dir).resolve()
    file_glob = Path(working_dir, "*.JPG")
    output_dir = Path(args.output_dir).resolve()

    total = int(args.rotate / 90)

    width = args.width
    height = args.height
    scale = f"{width}x{height}"

    outfilename = working_dir.name
    if args.name:
        outfilename = args.name

    rotation = ""

    if total:
        rotation = ",".join(["transpose=2" for i in range(0, total)])
        rotation = f"-vf \"{rotation}\""

    command = [
        "ffmpeg",
        "-f",
        "image2",
        "-pattern_type",
        "glob",
        "-r",
        f"{args.fps}",
        "-i",
        f"{file_glob}",
        "-s",
        f"{scale}",
        "-c:v",
        "libx264",
        "-q:v",
        "10",
        "-an",
    ]

    if rotation:
        command.append("rotation")

    command.append(str(Path(output_dir, f"{outfilename}.mp4")))

    subprocess.run(command, check=True)


parser = argparse.ArgumentParser(
    description="A utility to help facilitate GoPro file management.",
)

subparsers = parser.add_subparsers(
    help="sub-command help",
    dest="command",
)

organize_parser = subparsers.add_parser(
    "organize",
    aliases=["o"],
    help="Organize the GoPro files",
)
organize_parser.add_argument(
    "directory",
    type=str,
    nargs="+",
    help="the directory that needs files to be organized",
    default=".",
)
organize_parser.add_argument(
    "--timelapse-dir",
    type=str,
    help="the directory to store timelapse files",
    default="./timelapse",
)
organize_parser.add_argument(
    "--video-dir",
    type=str,
    help="the directory to store video files",
    default="./videos",
)
organize_parser.add_argument(
    "--model",
    type=str,
    help="the model of go pro used",
    default="hero7",
)
organize_parser.add_argument(
    "--archive",
    type=str_to_bool,
    help="Enable or disable archiving the timelapse files",
    default=True,
)

timelapse_parser = subparsers.add_parser(
    "timelapse",
    help="Compile timelapse",
)
timelapse_parser.add_argument(
    "dir",
    type=str,
    help="the directory where the timelapse frames are",
    default=".",
)
timelapse_parser.add_argument(
    "--output-dir",
    type=str,
    help="the directory to store the file",
    default=".",
)
timelapse_parser.add_argument(
    "--rotate",
    type=int,
    help="the amount to rotate the video by",
    default=0,
)
timelapse_parser.add_argument(
    "--width",
    type=int,
    help="the final width of the video",
    default=4000,
)
timelapse_parser.add_argument(
    "--height",
    type=int,
    help="the final height of the video",
    default=3000,
)
timelapse_parser.add_argument(
    "--fps",
    type=int,
    help="the frame rate of the video",
    default=6,
)
timelapse_parser.add_argument(
    "--name",
    type=str,
    help="the output file without the extension",
    default=None,
)

args = parser.parse_args()

COMMANDS = {
    "organize": organize,
    "timelapse": timelapse,
}

COMMANDS[args.command](args)