Skip to content

Instantly share code, notes, and snippets.

@noelleleigh
Last active December 23, 2023 01:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noelleleigh/2a8cc16ce60376ae79bf6a5a1b89f7f6 to your computer and use it in GitHub Desktop.
Save noelleleigh/2a8cc16ce60376ae79bf6a5a1b89f7f6 to your computer and use it in GitHub Desktop.
Python CLI script for using the concat demuxer in ffmpeg (https://trac.ffmpeg.org/wiki/Concatenate)
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.info("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.info("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.info("FFmpeg invocation:\n%s", " ".join(args))
return subprocess.run(args)
if __name__ == "__main__":
def path_type(val: str) -> Path:
"""Make a value an absolute Path."""
return Path(val).absolute()
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.
Bash example:
python ffmpeg_concat.py -o ./album.mp3 ./album/*.mp3
Powershell example:
python ffmpeg_concat.py -o ./album.mp3 (Get-ChildItem ./album/*.mp3)
"""
),
)
parser.add_argument(
"input_file",
type=path_type,
nargs="+",
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",
required=False,
help="Overwrite the output file if it already exists.",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
required=False,
help="Output more information to help with troubleshooting.",
)
parser.add_argument(
"--chapters",
action="store_true",
required=False,
help="Include unnamed chapters for each input file.",
)
parser.add_argument(
"--keep-script",
action="store_true",
required=False,
help=("Don't delete the concat script when the program exits."),
)
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.INFO)
logger.info("Parsed args:\n%s", args)
main(
input_file_paths=args.input_file,
output_file_path=args.output,
overwrite=args.overwrite,
include_chapters=args.chapters,
keep_script=args.keep_script,
)
import argparse
import logging
import subprocess
from pathlib import Path
logging.basicConfig()
logger = logging.getLogger("ffmpeg_split")
def make_part_name(path: Path, index: int):
return path.with_suffix(f".{index:02}{path.suffix}")
def main(
input_file_path: Path,
output_file_path: Path,
overwrite: bool,
start: str,
end: str,
):
part_1_path = make_part_name(output_file_path, 1)
part_2_path = make_part_name(output_file_path, 2)
args = (
("ffmpeg", "-hide_banner")
+ (("-y",) if overwrite else ())
+ ("-i", input_file_path)
+ ("-t", start)
+ ("-c", "copy")
+ (part_1_path,)
+ ("-ss", end)
+ ("-c", "copy")
+ (part_2_path,)
)
logger.info("FFmpeg invocation: %s", " ".join(map(str, args)))
subprocess.run(args)
return [part_1_path, part_2_path]
if __name__ == "__main__":
logger.setLevel(logging.INFO)
def path_type(val: str) -> Path:
"""Make a value an absolute Path."""
return Path(val).absolute()
parser = argparse.ArgumentParser(
description="Split a file into two pieces with a part in the middle removed. "
"Meant to remove midroll ads."
)
parser.add_argument("input_file", type=path_type, help="Input file")
parser.add_argument(
"--output",
"-o",
type=path_type,
required=True,
help="Output file. "
'The script will insert "01" and "02" before the file extension on both parts. '
'Example: "foo.mp3" will result in "foo.01.mp3" and "foo.02.mp3"',
)
parser.add_argument(
"--start",
"-t",
required=True,
help="The timestamp of when to start cutting "
"(Format: https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax)",
)
parser.add_argument(
"--end",
"-ss",
required=True,
help="The timestamp of when to stop cutting "
"(Format: https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax)",
)
parser.add_argument(
"--overwrite",
"-y",
action="store_true",
required=False,
help="Overwrite the output file if it already exists.",
)
args = parser.parse_args()
output_paths = main(
input_file_path=args.input_file,
output_file_path=args.output,
overwrite=args.overwrite,
start=args.start,
end=args.end,
)
for path in output_paths:
print(path)
@noelleleigh
Copy link
Author

noelleleigh commented Jul 18, 2021

usage: ffmpeg_concat.py [-h] --output OUTPUT [--overwrite] [--verbose] [--chapters] [--keep-script] input_file [input_file ...]

Concatenate media files of identical formats together.
Requires ffmpeg to be installed and available on the PATH.

Bash example:

    python ffmpeg_concat.py -o ./album.mp3 ./album/*.mp3

Powershell example:

    python ffmpeg_concat.py -o ./album.mp3 (Get-ChildItem ./album/*.mp3)

positional arguments:
  input_file            Input file(s)

options:
  -h, --help            show this help message and exit
  --output OUTPUT, -o OUTPUT
                        Output file
  --overwrite, -y       Overwrite the output file if it already exists.
  --verbose, -v         Output more information to help with troubleshooting.
  --chapters            Include unnamed chapters for each input file.
  --keep-script         Don't delete the concat script when the program exits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment