335 lines
10 KiB
Bash
Executable File
335 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Copyright 2021-2022 https://jnktn.tv
|
|
# Release under the terms of BSD-3-Clause (see LICENSE)
|
|
# CLI parser generated using Argbash v2.9.0 (BSD-3-Clause) https://github.com/matejak/argbash
|
|
|
|
|
|
version="0.5"
|
|
|
|
|
|
|
|
# default initializations
|
|
_arg_input=
|
|
_arg_break_time=()
|
|
_arg_skip=
|
|
_arg_no_skip=false
|
|
_arg_no_transcode=false
|
|
_arg_dry_run=false
|
|
|
|
color_red="\033[31;49;1m"
|
|
color_blue="\033[34;49;1m"
|
|
color_yellow="\033[93;49;1m"
|
|
color_reset="\033[0m"
|
|
|
|
die()
|
|
{
|
|
local _ret="${2:-1}"
|
|
test "${_PRINT_HELP:-no}" = yes && print_help >&2
|
|
echo -e "${color_red}ERROR: $1${color_reset}" >&2
|
|
exit "${_ret}"
|
|
}
|
|
|
|
print_help()
|
|
{
|
|
printf '%s\n' "jnk_cutter: splits the input video files into segments using the defined break times and also extracts the audio to mp3 format"
|
|
printf 'Usage: %s [-i|--input <arg>] [-o|--output-prefix <arg>] [--output-files <arg>] [-t|--break_time <arg>] [-s|--skip <arg>] [--no-skip] [--no-transcode] [--dry-run] [-h|--help] [-v|--version]\n' "$0"
|
|
printf '\t%s\n' "-i, --input: input file name (no default)"
|
|
printf '\t%s\n' "-o, --output-prefix: output files name prefix (optional. default_prefix = input file name)"
|
|
printf '\t%s\n' "--output-files: comma-separated output file names"
|
|
printf '\t%s\n' "-t, --break_time: time in the ffmpeg time format e.g. hh:mm:ss to split the file"
|
|
printf '\t%s\n' "-s, --skip: comma-separated list of section numbers to skip, 0-based (optional. default = every other section starting from 0)"
|
|
printf '\t%s\n' "--no-skip: do not skip any section"
|
|
printf '\t%s\n' "--no-transcode: do not transcode the video"
|
|
printf '\t%s\n' "--dry-run: do not execute ffmpeg commands"
|
|
printf '\t%s\n' "-h, --help: Prints help"
|
|
printf '\t%s\n' "-v, --version: Prints version"
|
|
}
|
|
|
|
|
|
parse_commandline()
|
|
{
|
|
while test $# -gt 0
|
|
do
|
|
_key="$1"
|
|
case "$_key" in
|
|
-i|--input)
|
|
test $# -lt 2 && die "Missing value for the argument '$_key'."
|
|
_arg_input="$2"
|
|
shift
|
|
;;
|
|
--input=*)
|
|
_arg_input="${_key##--input=}"
|
|
;;
|
|
-i*)
|
|
_arg_input="${_key##-i}"
|
|
;;
|
|
-o|--output-prefix)
|
|
test $# -lt 2 && die "Missing value for the optional argument '$_key'."
|
|
_arg_output_prefix="$2"
|
|
shift
|
|
;;
|
|
--output-prefix=*)
|
|
_arg_output_prefix="${_key##--output-prefix=}"
|
|
;;
|
|
-o*)
|
|
_arg_output_prefix="${_key##-o}"
|
|
;;
|
|
--output-files)
|
|
test $# -lt 2 && die "Missing value for the optional argument '$_key'."
|
|
_arg_output_files="$2"
|
|
shift
|
|
;;
|
|
--output-files=*)
|
|
_arg_output_files="${_key##--output-files=}"
|
|
;;
|
|
-s|--skip)
|
|
test $# -lt 2 && die "Missing value for the optional argument '$_key'."
|
|
_arg_skip="$2"
|
|
shift
|
|
;;
|
|
--skip=*)
|
|
_arg_skip="${_key##--skip=}"
|
|
;;
|
|
-s*)
|
|
_arg_skip="${_key##-s}"
|
|
;;
|
|
-t|--break_time)
|
|
test $# -lt 2 && die "Missing value for the optional argument '$_key'."
|
|
_arg_break_time+=("$2")
|
|
shift
|
|
;;
|
|
--break_time=*)
|
|
_arg_break_time+=("${_key##--break_time=}")
|
|
;;
|
|
-t*)
|
|
_arg_break_time+=("${_key##-t}")
|
|
;;
|
|
--no-skip)
|
|
_arg_no_skip=true
|
|
;;
|
|
--no-transcode)
|
|
_arg_no_transcode=true
|
|
;;
|
|
--dry-run)
|
|
_arg_dry_run=true
|
|
;;
|
|
-h|--help)
|
|
print_help
|
|
exit 0
|
|
;;
|
|
-h*)
|
|
print_help
|
|
exit 0
|
|
;;
|
|
-v|--version)
|
|
echo jnk_cutter v$version
|
|
exit 0
|
|
;;
|
|
-v*)
|
|
echo jnk_cutter v$version
|
|
exit 0
|
|
;;
|
|
*)
|
|
_PRINT_HELP=yes die "Got an unexpected argument '$1'"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
parse_commandline "$@"
|
|
|
|
ffmpeg_execs=( 'ffmpeg' 'ffmpeg.exe' './ffmpeg' './ffmpeg.exe' )
|
|
|
|
for _ffmpeg_exec in "${ffmpeg_execs[@]}"; do
|
|
if [[ -x "$(command -v "$_ffmpeg_exec")" ]]; then
|
|
ffmpeg_exec="$_ffmpeg_exec"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$ffmpeg_exec" ]; then
|
|
die "ffmpeg was not found! Please install ffmpeg before using the jnk_cutter."
|
|
fi
|
|
|
|
if [ -z "${_arg_input}" ]; then
|
|
die "Input file is not defined!"
|
|
fi
|
|
|
|
if [ ! -f "${_arg_input}" ]; then
|
|
die "${_arg_input} could not be found!"
|
|
fi
|
|
|
|
if [ "$_arg_no_skip" = true ] && [ -n "${_arg_skip}" ]; then
|
|
die "--no-skip and --skip flags cannot be used together! To override the default section skips just use --skip flag."
|
|
fi
|
|
|
|
skip_sections=()
|
|
skip_sections_unique=()
|
|
if [ "$_arg_no_skip" = false ] ; then
|
|
if [ -z "${_arg_skip}" ]; then
|
|
for ((index=1;index<=${#_arg_break_time[@]}+1;index+=2)); do
|
|
skip_sections+=("$index")
|
|
done
|
|
else
|
|
IFS=',' read -ra skip_sections <<< "$_arg_skip"
|
|
fi
|
|
fi
|
|
|
|
# de-duplicate the skip sections number
|
|
if [ ${#skip_sections[@]} != 0 ]; then
|
|
mapfile -t skip_sections_unique <<< "$(printf "%s\n" "${skip_sections[@]}" | sort -u)"
|
|
fi
|
|
|
|
all_sections_count=$((${#_arg_break_time[@]}+1))
|
|
for skip_section_index in "${skip_sections_unique[@]}"; do
|
|
if [[ ${skip_section_index} -gt ${all_sections_count} ]]; then
|
|
die "Skipping section ${skip_section_index} is not possible! There are only ${all_sections_count} sections."
|
|
fi
|
|
if [[ ${skip_section_index} -le 0 ]]; then
|
|
die "Skipping section ${skip_section_index} is not possible! The section number must be > 0"
|
|
fi
|
|
done
|
|
|
|
output_sections_count=$((${#_arg_break_time[@]}+1-${#skip_sections_unique[@]}))
|
|
input_file_base=${_arg_input%.*}
|
|
input_file_ext=${_arg_input#"$input_file_base".}
|
|
|
|
if [ -z "${_arg_output_files}" ]; then # --output_files is missing
|
|
if [ -z "${_arg_output_prefix}" ]; then # ...and --output_prefix is missing
|
|
processed_output_prefix=${_arg_input}
|
|
else
|
|
processed_output_prefix=${_arg_output_prefix}
|
|
fi
|
|
|
|
# generate output file names from the prefix
|
|
output_base=${processed_output_prefix%.*}
|
|
output_ext=${processed_output_prefix#"$output_base".}
|
|
if [ "$output_ext" == "$output_base" ];then
|
|
output_ext="$input_file_ext"
|
|
fi
|
|
for ((file_i=1; file_i <= output_sections_count ; file_i++)) ; do
|
|
output_files[file_i-1]=${output_base}_${file_i}.${output_ext}
|
|
done
|
|
|
|
else
|
|
if [ -z "${_arg_output_prefix}" ]; then
|
|
IFS=',' read -ra output_files <<< "$_arg_output_files"
|
|
|
|
if [ $output_sections_count != ${#output_files[@]} ]; then
|
|
echo -e "${color_red}ERROR: Number of the required filenames for the output files does not match the number of provided filenames!"
|
|
echo "> Required number of filenames: $output_sections_count"
|
|
echo -e "> Provided number of filenames: ${#output_files[@]}${color_reset}"
|
|
exit 1
|
|
fi
|
|
else
|
|
die "Only one of --output-prefix or --output-files options should be defined!"
|
|
fi
|
|
fi
|
|
|
|
|
|
# if the generated output video filenames don't have ext, add ext of the input video file
|
|
|
|
for ((file_i=0; file_i < output_sections_count; file_i++)) ; do
|
|
case ${output_files[file_i]} in
|
|
(*.*) ;;
|
|
(*) output_files[file_i]="${output_files[file_i]}.$input_file_ext";;
|
|
esac
|
|
done
|
|
|
|
if [ ${_arg_no_transcode} = "true" ]; then
|
|
transcode="No"
|
|
ffmpeg_video_flags=("-c:v" "copy")
|
|
else
|
|
transcode="Yes"
|
|
ffmpeg_video_flags=("-preset" "veryfast" "-s" "1280x720")
|
|
fi
|
|
|
|
echo "----------------------------------------------"
|
|
echo "ffmpeg path : $ffmpeg_exec"
|
|
echo "Input file : $_arg_input"
|
|
echo "Output file names : ${output_files[*]}"
|
|
echo "Output files prefix : ${_arg_output_prefix}"
|
|
echo "Sections break time : ${_arg_break_time[*]}"
|
|
echo "Skip sections : ${skip_sections_unique[*]}"
|
|
echo "Output sections count : $output_sections_count"
|
|
echo "Transcode : ${transcode}"
|
|
echo "----------------------------------------------"
|
|
|
|
# parse and validate input times
|
|
break_time_parsed_0=0 # time of the former break in seconds
|
|
ffmpeg_time_regex='^([0-9]{1,2}:)?([0-9]{1,2}:)?([0-9]{1,2})(\.[0-9]{1,3})?' # [[HH:]MM:]SS[.ms]
|
|
for break_time in "${_arg_break_time[@]}"; do
|
|
[[ ${break_time} =~ ${ffmpeg_time_regex} ]]
|
|
if [[ "${break_time}" == "${BASH_REMATCH[0]}" ]]; then
|
|
echo "Break time ${break_time} matches the ffmpeg format."
|
|
else
|
|
die "Break time ${break_time} does NOT match the ffmpeg format!"
|
|
fi
|
|
|
|
if [ -n "${BASH_REMATCH[1]}" ] && [ -z "${BASH_REMATCH[2]}" ]; then
|
|
break_time_min=${BASH_REMATCH[1]/:/}
|
|
else
|
|
break_time_hour=${BASH_REMATCH[1]/:/}
|
|
break_time_min=${BASH_REMATCH[2]/:/}
|
|
fi
|
|
break_time_hour=${break_time_hour:=0}
|
|
break_time_min=${break_time_min:=0}
|
|
break_time_sec=${BASH_REMATCH[3]}
|
|
break_time_msec=${BASH_REMATCH[4]/./}
|
|
break_time_msec=${break_time_msec:=0}
|
|
|
|
echo "> Parsed as: hour = ${break_time_hour} min = ${break_time_min} sec = ${break_time_sec}.${break_time_msec}"
|
|
|
|
if [ "${break_time_min}" -gt 59 ]; then
|
|
die "time ${break_time} is not valid! (min > 59)"
|
|
fi
|
|
|
|
if [ "${break_time_sec}" -gt 59 ]; then
|
|
die "time ${break_time} is not valid! (sec > 59)"
|
|
fi
|
|
|
|
if [ "${break_time_msec}" -gt 999 ]; then
|
|
die "time ${break_time} is not valid! (msec > 999)"
|
|
fi
|
|
|
|
if [[ -x "$(command -v bc)" ]]; then
|
|
break_time_parsed=$(echo "${break_time_hour}*3600+${break_time_min}*60+${break_time_sec}.${break_time_msec}" | bc -l )
|
|
if [ "$(echo "$break_time_parsed < $break_time_parsed_0" | bc -l)" -ne 0 ];then
|
|
die "The order of break times is not correct! Check: ${break_time}"
|
|
fi
|
|
break_time_parsed_0=$break_time_parsed
|
|
else
|
|
echo -e "${color_yellow}WARNING: bc was not found! Some checks will be skipped.${color_reset}"
|
|
fi
|
|
done
|
|
|
|
output_file_i=0
|
|
section_i=1
|
|
break0="00:00:00"
|
|
|
|
for break_time in "${_arg_break_time[@]}"; do
|
|
if [[ ! "${skip_sections_unique[*]}" =~ ${section_i} ]]; then
|
|
echo -e "${color_blue}Cutting ${break0} till ${break_time}${color_reset}"
|
|
output_file=${output_files[output_file_i]}
|
|
echo -e "${color_blue}${ffmpeg_exec} -i ${_arg_input} -ss ${break0} -to ${break_time} ${ffmpeg_video_flags[*]} -c:a copy ${output_file}${color_reset}"
|
|
[ "$_arg_dry_run" = true ] || command "${ffmpeg_exec}" -i "${_arg_input}" -ss "${break0}" -to "${break_time}" "${ffmpeg_video_flags[@]}" -c:a copy "${output_file}"
|
|
echo -e "${color_blue}${ffmpeg_exec} -i ${output_file} -b:a 320k ${output_file%.*}.mp3${color_reset}"
|
|
[ "$_arg_dry_run" = true ] || command "${ffmpeg_exec}" -i "${output_file}" -b:a 320k "${output_file%.*}.mp3"
|
|
output_file_i=$((output_file_i+1))
|
|
fi
|
|
break0=${break_time}
|
|
section_i=$((section_i+1))
|
|
done
|
|
|
|
if [[ ! "${skip_sections_unique[*]}" =~ ${section_i} ]]; then
|
|
# the last segments is processed searately
|
|
output_file=${output_files[output_file_i]}
|
|
echo -e "${color_blue}Cutting ${break0} till the end${color_reset}"
|
|
echo -e "${color_blue}${ffmpeg_exec} -i ${_arg_input} -ss ${break0} ${ffmpeg_video_flags[*]} -c:a copy ${output_file}${color_reset}"
|
|
[ "$_arg_dry_run" = true ] || command "${ffmpeg_exec}" -i "${_arg_input}" -ss "${break0}" "${ffmpeg_video_flags[@]}" -c:a copy "${output_file}"
|
|
echo -e "${color_blue}${ffmpeg_exec} -i ${output_file} -b:a 320k ${output_file%.*}.mp3${color_reset}"
|
|
[ "$_arg_dry_run" = true ] || command "${ffmpeg_exec}" -i "${output_file}" -b:a 320k "${output_file%.*}.mp3"
|
|
fi
|