Last active
January 10, 2025 02:14
-
-
Save noelleleigh/2a8cc16ce60376ae79bf6a5a1b89f7f6 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
| import argparse | |
| import logging | |
| import subprocess | |
| from collections.abc import Iterable | |
| from contextlib import contextmanager | |
| from inspect import cleandoc | |
| from pathlib import Path | |
| from tempfile import NamedTemporaryFile | |
| logging.basicConfig() | |
| logger = logging.getLogger("ffmpeg_concat") | |
| def escape_path(path: Path) -> str: | |
| """ | |
| Escape a file path for a concat script. | |
| https://ffmpeg.org/ffmpeg-formats.html#Syntax | |
| """ | |
| return str(path).replace("'", "'\\''") | |
| def make_file_directive(path: Path) -> str: | |
| """ | |
| Make a file directive for a concat script. | |
| https://ffmpeg.org/ffmpeg-formats.html#Syntax | |
| """ | |
| escaped = escape_path(path) | |
| return f"file '{escaped}'" | |
| def get_file_duration(path: Path) -> float: | |
| """ | |
| Return the duration of a file in fractional seconds. | |
| Source: https://stackoverflow.com/a/22243834 | |
| """ | |
| args = ( | |
| ("ffprobe", "-hide_banner") | |
| + ("-show_entries", "format=duration") | |
| + ("-print_format", "csv=p=0") | |
| + (str(path),) | |
| ) | |
| logger.debug("ffprobe invocation:\n%s", " ".join(args)) | |
| result = subprocess.run( | |
| args=args, | |
| capture_output=True, | |
| check=True, | |
| text=True, | |
| ) | |
| return float(result.stdout) | |
| @contextmanager | |
| def make_concat_script( | |
| input_file_paths: Iterable[Path], | |
| include_chapters: bool = False, | |
| keep_script: bool = False, | |
| ): | |
| """ | |
| Context manager for making a temporary concat script. | |
| """ | |
| concat_script_lines = ["ffconcat version 1.0"] | |
| file_duration_pointer = 0.0 | |
| for file_num, file_path in enumerate(input_file_paths, start=1): | |
| concat_script_lines.append(make_file_directive(file_path)) | |
| if include_chapters: | |
| file_duration = get_file_duration(file_path) | |
| concat_script_lines.append( | |
| " ".join( | |
| ( | |
| "chapter", | |
| str(file_num), | |
| str(file_duration_pointer), | |
| str(file_duration_pointer + file_duration), | |
| ) | |
| ) | |
| ) | |
| file_duration_pointer = file_duration_pointer + file_duration | |
| # On Windows, the file can't be opened a second time while it is open for creation. | |
| # So we close the file after writing, and handle deletion after the fact. | |
| with NamedTemporaryFile( | |
| mode="w+t", encoding="utf-8", suffix=".txt", delete=False | |
| ) as concat_script: | |
| concat_script_body = "\n".join(concat_script_lines) | |
| logger.debug("concat script:\n%s", concat_script_body) | |
| concat_script.write(concat_script_body) | |
| try: | |
| yield concat_script.name | |
| finally: | |
| if not keep_script: | |
| # Delete the file when we're done with it. | |
| Path(concat_script.name).unlink() | |
| def main( | |
| input_file_paths: Iterable[Path], | |
| output_file_path: Path, | |
| overwrite: bool = False, | |
| include_chapters: bool = False, | |
| keep_script: bool = False, | |
| ): | |
| with make_concat_script( | |
| input_file_paths, | |
| include_chapters=include_chapters, | |
| keep_script=keep_script, | |
| ) as concat_script_path: | |
| args = ( | |
| ("ffmpeg", "-hide_banner") | |
| + (("-y",) if overwrite else ()) | |
| + ("-f", "concat") | |
| + ("-safe", "0") | |
| + ("-i", concat_script_path) | |
| + ("-c", "copy") | |
| + (str(output_file_path),) | |
| ) | |
| logger.debug("FFmpeg invocation:\n%s", subprocess.list2cmdline(args)) | |
| return subprocess.run(args, encoding="utf-8", check=True) | |
| if __name__ == "__main__": | |
| def path_type(val: str) -> Path: | |
| """Make a value an absolute Path.""" | |
| return Path(val).resolve() | |
| parser = argparse.ArgumentParser( | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| description=cleandoc( | |
| """ | |
| Concatenate media files of identical formats together. | |
| Requires ffmpeg to be installed and available on the PATH. | |
| Uses the concat demuxer (https://ffmpeg.org/ffmpeg-formats.html#concat-1) | |
| under the hood. | |
| """ | |
| ), | |
| epilog=cleandoc( | |
| """ | |
| Bash example: | |
| python ffmpeg_concat.py -i ./album/*.mp3 -o ./album.mp3 | |
| Powershell example: | |
| python ffmpeg_concat.py -i (Get-ChildItem ./album/*.mp3) -o ./album.mp3 | |
| """ | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--input", | |
| "-i", | |
| type=path_type, | |
| nargs="+", | |
| required=True, | |
| help="Input file(s)", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| "-o", | |
| type=path_type, | |
| required=True, | |
| help="Output file", | |
| ) | |
| parser.add_argument( | |
| "--overwrite", | |
| "-y", | |
| action="store_true", | |
| help="Overwrite the output file if it already exists.", | |
| ) | |
| parser.add_argument( | |
| "--verbose", | |
| "-v", | |
| action="store_true", | |
| help="Output more information to help with troubleshooting.", | |
| ) | |
| parser.add_argument( | |
| "--chapters", | |
| action="store_true", | |
| help="Include unnamed chapters for each input file.", | |
| ) | |
| parser.add_argument( | |
| "--keep-script", | |
| action="store_true", | |
| help=("Don't delete the concat script when the program exits."), | |
| ) | |
| args = parser.parse_args() | |
| logger.setLevel(logging.DEBUG if args.verbose else logging.INFO) | |
| logger.debug("Parsed arguments:\n%s", args) | |
| main( | |
| input_file_paths=args.input, | |
| output_file_path=args.output, | |
| overwrite=args.overwrite, | |
| include_chapters=args.chapters, | |
| keep_script=args.keep_script, | |
| ) |
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
| import argparse | |
| import logging | |
| import subprocess | |
| from inspect import cleandoc | |
| from pathlib import Path | |
| logging.basicConfig() | |
| logger = logging.getLogger("ffmpeg_split") | |
| def main( | |
| input_file_path: Path, | |
| output_file_path: Path, | |
| split_times: list[str], | |
| ): | |
| args = ( | |
| ("ffmpeg", "-hide_banner") | |
| + ("-i", str(input_file_path)) | |
| + ("-c", "copy") | |
| + ("-f", "segment") | |
| + ("-segment_times", ",".join(split_times)) | |
| + ("-segment_start_number", "1") | |
| + ("-reset_timestamps", "1") | |
| + (str(output_file_path),) | |
| ) | |
| logger.debug("FFmpeg invocation:\n%s", subprocess.list2cmdline(args)) | |
| return subprocess.run(args, encoding="utf-8", check=True) | |
| def path_type(val: str) -> Path: | |
| """Make a value an absolute Path.""" | |
| return Path(val).resolve() | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| description=cleandoc( | |
| """ | |
| Split a file into two or more segments at specific timestamps. | |
| Requires ffmpeg to be installed and available on the PATH. | |
| Uses the segment muxer (https://ffmpeg.org/ffmpeg-formats.html#segment) | |
| under the hood. | |
| """, | |
| ), | |
| epilog=cleandoc( | |
| """ | |
| Example: | |
| python ffmpeg_split.py -i './album.mp3' -o './album/track %02d.mp3' --split-times 01:23 5:00 11:04 | |
| """ | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--input", | |
| "-i", | |
| type=path_type, | |
| required=True, | |
| help="Input file", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| "-o", | |
| type=path_type, | |
| required=True, | |
| help=( | |
| "The output filenames are specified by a pattern, which can be used to " | |
| "produce sequentially numbered series of files. " | |
| 'The pattern may contain the string "%%d" or "%%0Nd", this string ' | |
| "specifies the position of the characters representing a numbering in the " | |
| "filenames. " | |
| 'If the form "%%0Nd" is used, the string representing the number in each ' | |
| "filename is 0-padded to N digits. " | |
| "The literal character ’%%’ can be specified in the pattern with the " | |
| 'string "%%%%".\n' | |
| "More format details: https://ffmpeg.org/ffmpeg-formats.html#image2" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--split-times", | |
| nargs="+", | |
| required=True, | |
| help=( | |
| "The timestamp of when to split the file in [-][HH:]MM:SS[.m...] or " | |
| "[-]S+[.m...][s|ms|us] formats.\n" | |
| "Format details: https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--verbose", | |
| "-v", | |
| action="store_true", | |
| help="Output more information to help with troubleshooting.", | |
| ) | |
| args = parser.parse_args() | |
| logger.setLevel(logging.DEBUG if args.verbose else logging.INFO) | |
| logger.debug("Parsed arguments:\n%s", args) | |
| main( | |
| input_file_path=args.input, | |
| output_file_path=args.output, | |
| split_times=args.split_times, | |
| ) |
Author
usage: ffmpeg_split.py [-h] --input INPUT --output OUTPUT --split-times SPLIT_TIMES
[SPLIT_TIMES ...] [--verbose]
Split a file into two or more segments at specific timestamps.
Requires ffmpeg to be installed and available on the PATH.
Uses the segment muxer (https://ffmpeg.org/ffmpeg-formats.html#segment)
under the hood.
options:
-h, --help show this help message and exit
--input INPUT, -i INPUT
Input file
--output OUTPUT, -o OUTPUT
The output filenames are specified by a pattern, which can be used to
produce sequentially numbered series of files. The pattern may contain the
string "%d" or "%0Nd", this string specifies the position of the characters
representing a numbering in the filenames. If the form "%0Nd" is used, the
string representing the number in each filename is 0-padded to N digits. The
literal character ’%’ can be specified in the pattern with the string "%%".
More format details: https://ffmpeg.org/ffmpeg-formats.html#image2
--split-times SPLIT_TIMES [SPLIT_TIMES ...]
The timestamp of when to split the file in [-][HH:]MM:SS[.m...] or
[-]S+[.m...][s|ms|us] formats. Format details: https://ffmpeg.org/ffmpeg-
utils.html#time-duration-syntax
--verbose, -v Output more information to help with troubleshooting.
Example:
python ffmpeg_split.py -i './album.mp3' -o './album/track %02d.mp3' --split-times 01:23 5:00 11:04
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.