Last active
August 25, 2023 07:21
-
-
Save YUKI2eN3e/fc558d480b920d8dc4d39fec044c1b18 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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