#!/bin/bash
#set -n		# read but do not execute commands (check on syntax errors)
#
# File: /usr/local/bin/losslesscut
#
# Media lossless cut.
#
#    Copyright (c) 2020  Juergen Kaesmann (JK)
#
##############################################################################
# GPL NOTICE
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
##############################################################################
# FIND SAMPLES
#
##############################################################################
# FILTER SAMPLES
#
##############################################################################
# AWK SAMPLES
#
##############################################################################
# SED SAMPLES
#
##############################################################################
# RSYNC OPTIONS
#
##############################################################################
# DIALOG SAMPLES
#
##############################################################################
# REPLACE STRINGS
#
##############################################################################
# EXIT CODES

##############################################################################
# SOURCE FILES
#
. /usr/local/etc/main.conf				# config main
. $LLIBDIR/libfuncstd-sh				# functions standard
#. $LLIBDIR/libfuncmath2-sh				# functions math2
#. $LLIBDIR/libfuncip-sh				# functions IP
#. /usr/local/etc/sample.conf				# config sample
#
#if [ ! -e ~/.samplerc ]; then				# install config user
#	cp /usr/local/share/ksm-sample/samplerc ~/.samplerc
#fi
#. ~/.samplerc						# config user

##############################################################################
# DECLARATION OF STANDARD CONSTANTS AND VARIABLES
#
readonly package=Multimedia				# package name
readonly package_file=ksm-multimedia			# package file name
readonly vinfopath="/usr/local/share/$package_file"	# path to package dir
readonly version=$(/bin/cat $vinfopath/VERSION)		# package version
readonly release=$(/bin/cat $vinfopath/RELEASE)		# package release
readonly scriptname=$(basename $0)			# name of script
#readonly tempfile=$TEMPDIR/$scriptname.$UID.tmp	# temporary file name
#tempfile=$(mktemp $TEMPDIR/$scriptname.$UID.XXXXXX)	# temporary file name
#readonly lockfile=$TEMPDIR/lock/$scriptname.$UID.lock	# lock file name
readonly debuglog="/usr/bin/logger -p user.debug -t $scriptname -- DEBUG:"	# log dbg msg
readonly errlog="/usr/bin/logger -p user.err -t $scriptname -- ERROR:"		# log err msg
readonly infolog="/usr/bin/logger -p user.info -t $scriptname -- INFO:"		# log inf msg
cmd=''							# sub command
debug=no						# debug=yes/no
verbose=''						# verbose=yes/''/no
err=${_ERR_OK_}						# error code

##############################################################################
# USER FILL IN AT FIRST RUN

##############################################################################
# SYSTEM

##############################################################################
# USER-CONSTANTS

##############################################################################
# USER-VARIABLES
#
# STRINGS
#args=''				# all args from cmdline
arg1=''					# 1. arg from cmdline
arg2=''					# 2. arg from cmdline
#opt_o=''				# option -o (yes)
					# option --output (yes)
#opt_i=''				# option -i (arg)
					# option --infile (arg)
#opt_o=''				# option -o (arg)
					# option --outfile (arg)
#opt_p=''				# option -p (arg)
					# option --pages (arg)
opt_adur=''				# option --adur (arg)
opt_probs=''				# option --probs (arg)
opt_ss=''				# option --ss (arg)
opt_ssi=''				# option --ssi (arg)
opt_t=''				# option --t (arg)
opt_to=''				# option --to (arg)

# NUMBERS
#p_first=0				# first page
#p_last=0				# last page

# ARRAYS
# Indexed arrays are always zero based string arrays.
# If index starts with 1 and all members are assigned then
# count of members of array = last index of array
#declare -a arr
#maxarr="${#arr[@]}"			# get number of elements in arr
#unset arr[@]				# remove the entire array

##############################################################################
# SIGNAL HANDLING

##############################################################################
# FUNCTIONS
##############################################################################
#
##############################################################################
# HELP
#
#=============================================================================
function helptext ()
{
# Output helptext
# Uses cat, echo, logger
# Return codes:
# Standard

if [ "$debug" == "yes" ]; then				# DEBUG
	echo "DEBUG helptext: $# $@" >&2
	$debuglog helptext: $# $@
fi

cat <<EOT
NAME
$scriptname - Media lossless cut.

SYNOPSIS
$scriptname [options] [inf outf]

DESCRIPTION
Media lossless cut. Allows you to cut at start points somewhere in a
GOP. Normally ffmpeg requires to start at the beginning of a GOP (start
at the I-Frame/IDR-Frame).

The input media file has to be of type MP4 (.mp4). The output media
file will be of type MP4 (.mp4), with video stream encoded as h264 and
audio stream (if any) encoded as aac. 

Cut lossless only the smallest necessary part which we will not get
with normal extracting (pay attention to the loss of quallity due to
recoding). 

Recoding is done from begin of <inf> not from the start time. Therefore
start time should not be to far away from begin of <inf>. Otherwise it
will take a lot of time.

Give at least one of --t or --to, but not both.

Defaults:  --ss = Begin of <inf>.

OPTIONS
Gnu style options:

  -h|--help    = Output help, then exit.
  -V|--version = Output version info, then exit.
  -d|--debug   = Turn on DEBUG mode.
  -q|--quiet   = Be quiet.
  -v|--verbose = Be verbose.
  -n           = No audio stream in <inf>. <inf> has only one video
                 stream.
  --adur=dur   = Analyzeduration (input).
                 dur in microseconds (100M|...). Default = 5000000
  --probs=siz  = Probesize (input).
                 siz in Bytes (32 - n, 100M|...). Default = 5000000
  --ss=start   = Start at time <start> (output option).
  		 start=seconds|SS:MM:SS[.dec] (dec = 1-3 digits).
  --ssi=start  = Start at time <start> (input option).
  		 start=seconds|SS:MM:SS[.dec] (dec = 1-3 digits).
  --t=dur      = Duration is <dur> (output option).
                 dur=seconds|SS:MM:SS[.dec] (dec = 1-3 digits).
                 Give only --t or --to.
  --to=end     = End at time <end> (output option).
                 end=seconds|SS:MM:SS[.dec] (dec = 1-3 digits).
                 Give only --t or --to.
  --           = End of options.

ARGUMENTS
Give if needed:

  inf  = Media Input file (.mp4)
  outf = Media Output file (.mp4)

EXAMPLES
For instance:

  Extract 3 seconds from infile.mp4, starting at 00:00:07.
  Output goes to outfile.mp4:
  $scriptname --ss=00:00:07 --to=00:00:10 infile.mp4 outfile.mp4
  or
  $scriptname --ss=7 --t=3 infile.mp4 outfile.mp4

EXIT STATUS
On program exit:

  Code 0 = OK

  See output of 'showerrmsg'.

SEE ALSO
Related manpages:

  showerrmsg(1), mediacut(1), mediaxcut(1), ffmpeg(1).

AUTHORS
Juergen Kaesmann <juergen@kaesmann-online.de>
EOT
#  -o|--output  = Output to STDOUT.
#  -i f         = Input from file <f>.
#  --infile=f   = Input from file <f>.
#  -o f         = Output to file <f>.
#  --outfile=f  = Output to file <f>.
#  -p s,e       = Pages s=START e=END.
#  --pages=s,e  = Pages s=START e=END.

#FILES
#Used directories and files:
#
#  Config Global = /usr/local/etc/sample.conf
#  Config User   = ~/.samplerc

return
}

#=============================================================================
function mainproc ()
{
# Do it.
# par1 = 
# Uses echo, logger
# Return codes:
# Standard

if [ "$debug" == "yes" ]; then				# DEBUG
	echo "DEBUG mainproc: $# $@" >&2
	$debuglog mainproc: $# $@
fi

local ret=0
#local -a arr
#maxarr="${#arr[@]}"			# get number of elements in arr
#unset arr[@]				# remove the entire array

if [ "$cmd" == -n ]; then				# inf has no audio
	case "$verbose" in
	    yes)
		echo "Recode from start of file"
		# allow STDOUT and STDERR
		ffmpeg	$opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-sn -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2
		ret=$?
		;;
	    '')
		#echo "Recode from start of file"
		# suppress STDERR
		ffmpeg	-hide_banner -y $opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-sn -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2 2>/dev/null
		ret=$?
		;;
	    no)
		# suppress STDOUT and STDERR
		ffmpeg	-hide_banner -y $opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-sn -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2 &>/dev/null
		ret=$?
		;;
	esac
else							# inf has audio
	case "$verbose" in
	    yes)
		echo "Recode from start of file"
		# allow STDOUT and STDERR
		ffmpeg	$opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-map 0:0 -map 0:1 -sn -c:a:0 aac -b:a:0 128k -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2
		ret=$?
		;;
	    '')
		#echo "Recode from start of file"
		# suppress STDERR
		ffmpeg	-hide_banner -y $opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-map 0:0 -map 0:1 -sn -c:a:0 aac -b:a:0 128k -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2 2>/dev/null
		ret=$?
		;;
	    no)
		# suppress STDOUT and STDERR
		ffmpeg	-hide_banner -y $opt_adur $opt_probs $opt_ssi -i $arg1 \
			 $opt_ss $opt_t -strict experimental -f mp4 -map_chapters -1 \
			-map 0:0 -map 0:1 -sn -c:a:0 aac -b:a:0 128k -c:v:0 libx264 \
			-b:v:0 1800k -mbd:v:0 rd -mbcmp:v:0 rd -cmp:v:0 rd -precmp:v:0 rd \
			-subcmp:v:0 rd -trellis:v:0 1 $opt_to $arg2 &>/dev/null
		ret=$?
		;;
	esac
fi
if [ "$ret" -ne 0 ]; then				# for other progs
	Show_Err ${_ERR_GENERAL_} "$ret from ffmpeg!"
	ret=${_ERR_GENERAL_}
fi

return $ret
}

##############################################################################
# GET OPTIONS
##############################################################################
# Give 2 strings and all cmdline_args to func GetOpt
# (POSIX_options Gnu_long_options cmdline_args):
# GetOpt 'hVdqvi:m:' 'help version debug quiet verbose infile= myval=' $@
#
# A ':' after a POSIX option means this option needs an arg.
# A '=' after a Gnu long option means this option needs an arg.
#
# All cmd/options have to be initialised to ''!
#
# POSIX options are classified in 2 categories:
#   a) cmds (mutually exclusive). All cmds are given to var 'cmd'.
#   b) options (as many as you like). Each option is given to it's own var.
# As a general rule: If cmds/options are given multiple then last given wins.
#
# Options can take 1 argument (no whitespace in arg). If you want to give more
# than 1 arg or want to use multiple strings as 1 arg then concatenate them
# together with a delimiter like ',' or ';' or '.'
# Use a delimiter which will not be part of the arg:
# sampleprog -m val1,val2
# sampleprog --myval=val1,val2
# It's at the programmers responsibility to do meaningfull things with the
# content of the options.
#
# If err != 0 then all args are processed, else continue to look for options.
# When GetOpt finishes then all options are removed from cmdline.
#
if [ "$debug" == "yes" ]; then				# DEBUG
	echo "DEBUG Parse Commandline: $# $@" >&2
	$debuglog Parse Commandline: $# $@
fi
#
err=${_ERR_OK_}
while [ "$err" -eq 0 ]; do
	GetOpt 'hVdqvn' 'help version debug quiet verbose adur= probs= ss= ssi= t= to=' $@
	err=$?
	#echo "DEBUG GET OPTIONS: err=${err}  _OPTNAME_=${_OPTNAME_}  _OPTVAL_=${_OPTVAL_}  _OPTPOS_=${_OPTPOS_}  _OPTIDX_=${_OPTIDX_}" >&2
	if [ $err -eq 0 ]; then
		case "$_OPTNAME_" in
		    h|help)	cmd="-h";;
		    V|version)	cmd="-V";;
		    n)		cmd="-${_OPTNAME_}";;
		    d|debug)	debug=yes;;
		    q|quiet)	verbose=no;;
		    v|verbose)	verbose=yes;;
#		    o|output)	opt_o=yes;;
#		    i|infile)	opt_i="${_OPTVAL_}";;
#		    o|outfile)	opt_o="${_OPTVAL_}";;
#		    p|pages)	opt_p="${_OPTVAL_}";;
		    adur)	opt_adur="-analyzeduration $_OPTVAL_";;
		    probs)	opt_probs="-probesize $_OPTVAL_";;
		    ss)		opt_ss="-ss ${_OPTVAL_}";;
		    ssi)	opt_ssi="-ss ${_OPTVAL_}";;
		    t)		opt_t="-t ${_OPTVAL_}";;
		    to)		opt_to="-to ${_OPTVAL_}";;
		    --)		break;;
		    :)	Show_Err ${_ERR_MISSING_ARG_FOR_OPT_} "-${_OPTVAL_}"
			exit ${_ERR_MISSING_ARG_FOR_OPT_};;
		    ?)	Show_Err ${_ERR_UNKNOWN_OPT_} "-${_OPTVAL_}"
			exit ${_ERR_UNKNOWN_OPT_};;
		    *)	Show_Err ${_ERR_UNKNOWN_ERR_} "_OPTNAME_=${_OPTNAME_} _OPTVAL_=${_OPTVAL_}"
			exit ${_ERR_UNKNOWN_ERR_};;
		esac
	fi
done
err=${_ERR_OK_}
shift ${_OPTIDX_}				# rm all opts from cmdline
#
# Uncomment if giving a cmd is mandatory
#if [ -z "$cmd" ]; then					# error no cmd given
#	Show_Err ${_ERR_MISSING_SUB_CMD_} ""
#	exit ${_ERR_MISSING_SUB_CMD_}
#fi

##############################################################################
# CHECK LINK NAMES

##############################################################################
# CHECK OPTIONS
#
#-----------------------------------------------------------------------------
# check not allowed option for sub commands
#case "$cmd" in
#    -l|-p)						# for these cmds
#	if [ -n "$opt_z" ]; then			# option -z not allowed
#		if [ "$verbose" != no ]; then
#			echo "${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -z" >&2
#		else
#			$errlog ${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -z
#		fi
#		exit ${_ERR_OPT_NOT_ALLOWED_}
#	fi
#	;;
#    *)							# for all other cmds
#	:
#	;;
#esac
#
#-----------------------------------------------------------------------------
# check allowed option for sub commands
#if [ -n "$opt_z" ]; then
#	case "$cmd" in
#	    -l|-p)					# for these cmds
#		:					# option -z allowed
#		;;
#	    *)						# for all other cmds
#		if [ "$verbose" != no ]; then
#			echo "${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -z" >&2
#		else
#			$errlog ${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -z
#		fi
#		exit ${_ERR_OPT_NOT_ALLOWED_}
#		;;
#	esac
#fi
#
#-----------------------------------------------------------------------------
# check needed option for sub commands
#case "$cmd" in
#    -l|-p)						# for these cmds
#	if [ -z "$opt_z" ]; then			# option -z needed
#		if [ "$verbose" != no ]; then
#			echo "${ErrMsg[${_ERR_MISSING_ARG_FOR_OPT_}]} -z" >&2
#		else
#			$errlog ${ErrMsg[${_ERR_MISSING_ARG_FOR_OPT_}]} -z
#		fi
#		exit ${_ERR_MISSING_ARG_FOR_OPT_}
#	fi
#	;;
#    *)							# for all other cmds
#	:
#	;;
#esac
#
#-----------------------------------------------------------------------------
# check options which are mutually exclusive
if [ -n "$opt_t" ] && [ -n "$opt_to" ]; then		# don't give both opts
		if [ "$verbose" != no ]; then
			echo "${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -t and -to given" >&2
		else
			$errlog ${ErrMsg[${_ERR_OPT_NOT_ALLOWED_}]} -t and -to given
		fi
		exit ${_ERR_OPT_NOT_ALLOWED_}
fi
#
#-----------------------------------------------------------------------------
# check option which needs another option
#if [ -n "$opt_to" ]; then			# opt_to given
#	if [ -z "$opt_ss" ]; then		# error - opt_to needs opt_ss
#		if [ "$verbose" != no ]; then
#			echo "${ErrMsg[${_ERR_GENERAL_}]} --to needs --ss" >&2
#		else
#			$errlog ${ErrMsg[${_ERR_GENERAL_}]} --to needs --ss
#		fi
#		exit ${_ERR_GENERAL_}
#	fi
#fi
#
#-----------------------------------------------------------------------------
# check options which needs to give at least one of them
case "$cmd" in
    ''|-n)
	if [ -z "$opt_t" ] && [ -z "$opt_to" ]; then # no option given
		if [ "$verbose" != no ]; then
			echo "${ErrMsg[${_ERR_GENERAL_}]} give --t or --to" >&2
		else
			$errlog ${ErrMsg[${_ERR_GENERAL_}]} give --t or --to
		fi
		exit ${_ERR_GENERAL_}
	fi
esac
#
#-----------------------------------------------------------------------------
# get values from arg of option -p to vars p_first p_last
#if [ -n "$opt_p" ]; then
#	p_first="${opt_p%,*}"
#	p_last="${opt_p#*,}"
#fi

##############################################################################
# CHECK VERSION

##############################################################################
# CHECK ARGUMENTS
#
case "$cmd" in
    ''|-n)
	if [ "$#" -eq 2 ]; then				# parameter count ok
		arg1="$1"
		arg2="$2"
		shift 2
		#args="$@"
		#shift $#
	else						# error in param count
		Show_Err ${_ERR_WRONG_PAR_COUNT_} ""
		exit ${_ERR_WRONG_PAR_COUNT_}
	fi
	;;
    *)
	if [ "$#" -ne 0 ]; then				# error in param count
		Show_Err ${_ERR_WRONG_PAR_COUNT_} ""
		exit ${_ERR_WRONG_PAR_COUNT_}
	fi
	;;
esac

##############################################################################
# LOCK

##############################################################################
# MAINPROGRAM
##############################################################################
#
if [ "$debug" == "yes" ]; then				# DEBUG
	echo "DEBUG Main: $# $@" >&2
	$debuglog Main: $# $@
fi

# Check what to do (sub commands, options and args are removed from cmdline)
case "$cmd" in
    ''|-n)
	if [ -z "$opt_ss" ]; then
		opt_ss=0
	fi

	if [ "${arg1##*.}" != mp4 ] || [ "${arg2##*.}" != mp4 ]; then # is not an .mp4
		Show_Err ${_ERR_GENERAL_} "Infile and outfile must be an mpeg4"
		exit ${_ERR_GENERAL_}
	fi

	mainproc
	err=$?
	;;
    -h)
	helptext
	exit 0
	;;
    -V)
	echo "${scriptname}: (${package}) $package_file ${version}-${release}"
	exit 0
	;;
    *)							# error
	Show_Err ${_ERR_SUB_CMD_NOT_FOUND_} ""
	err=${_ERR_SUB_CMD_NOT_FOUND_}
	;;
esac

#rm -f $lockfile

exit $err
