jnk_cutter/jnk_cutter.sh

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