Skip to content

Instantly share code, notes, and snippets.

@YUKI2eN3e
Last active August 25, 2023 07:21
Show Gist options
  • Select an option

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

Select an option

Save YUKI2eN3e/fc558d480b920d8dc4d39fec044c1b18 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
mediainfo, grep, and ffmpeg must be in your path for this to work
"""
import argparse
import subprocess as sp
from dataclasses import dataclass
from os import path
from typing import List
@dataclass
class CliArgs:
input_file: str
test: bool
def get_args() -> CliArgs:
parser = argparse.ArgumentParser(prog=path.basename(__file__))
parser.add_argument(
"-i",
"--input",
"--input-file",
dest="input_file",
help="video file to extract chapters from",
required=True,
)
parser.add_argument(
"-t",
"--test",
action="store_true",
dest="test",
default=False,
help="only list chapters, do not extract",
)
return CliArgs(**vars(parser.parse_args()))
def pad(num: int, padding=2) -> str:
return (
num if len(str(num)) >= padding else "0" * (padding - len(str(num))) + str(num)
)
class TimeStamp:
hours: int
minute: int
seconds: int
milliseconds: int
def __init__(self, timestamp: str) -> None:
parts = timestamp.split(":")
self.hours = int(parts[0])
self.minute = int(parts[1])
next_parts = parts[2].split(".")
self.seconds = int(next_parts[0])
self.milliseconds = int(next_parts[1])
def __repr__(self) -> str:
return f"{pad(self.hours)}:{pad(self.minute)}:{pad(self.seconds)}.{pad(self.milliseconds,3)}"
class Chapter:
start: TimeStamp
end: TimeStamp = None
title: str
def __init__(self, start: str, end: str | None, title: str) -> None:
self.start = TimeStamp(start)
if end is not None:
end_timestamp = TimeStamp(end)
end_timestamp.milliseconds = end_timestamp.milliseconds - 1
self.end = end_timestamp
self.title = title
def get_chapters(filename: str) -> List[Chapter]:
response = sp.run(
f'mediainfo "{filename}" | grep "^00.*"', capture_output=True, shell=True
)
chapter_strs = response.stdout.decode().splitlines()
chapters: List[Chapter] = []
for i in range(0, len(chapter_strs)):
if chapter_strs[i].strip() != "":
parts = chapter_strs[i].split(" :")
next_parts = (
chapter_strs[i + 1].split(" :") if i != len(chapter_strs) - 1 else None
)
chapters.append(
Chapter(
start=parts[0].strip(),
end=next_parts[0].strip() if next_parts is not None else None,
title=parts[1].strip().removeprefix(":"),
)
)
return chapters
def build_cmd(file: str, chapter: Chapter) -> List[str]:
cmd = ["ffmpeg", "-i", file, "-ss", str(chapter.start), "-c", "copy"]
if chapter.end is not None:
cmd.append("-to")
cmd.append(str(chapter.end))
cmd.append(f"{chapter.title}.{file.split('.')[-1]}")
return cmd
def run():
args = get_args()
chapters = get_chapters(args.input_file)
for chapter in chapters:
spacing = "\t" if len(chapter.title) >= 8 else "\t\t"
print(
f"{chapter.title}{spacing}{chapter.start}\t{chapter.end if chapter.end is not None else 'None'}"
)
if not args.test:
sp.run(build_cmd(file=args.input_file, chapter=chapter))
if __name__ == "__main__":
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment