#!/bin/sh -e
# Plays and records media clips (currently only audio clips).
#
# Example usage:
#
# Play clip specified by MediaClip.M2:
#   mediaclip.cgi?action=play&clip=2
#
# Wait 5 seconds and then record new clip during 10 seconds:
#   mediaclip.cgi?action=record&wait=5&duration=10
#
# Parameter 'generate_header' may be used to skip response header.

. /usr/html/axis-cgi/lib/functions.sh

readonly AUDIO_FILE_PATH=/etc/audioclips
readonly PHC="parhandclient --nocgi"
readonly SAMPLE_RATE_DEFAULT=16000
readonly LOGW="logger -pwarning -t${0##*/} -s"

# CGI compliant by default
CGI_HDGEN=$(__qs_getparam generate_header) || CGI_HDGEN=yes

printout() {
	if [ "$CGI_HDGEN" = no ]; then
		printf "%s\n" "$2"
		[ $# -lt 3 ] || printf "%s\n" "$3"
	else
		__cgi_errhd $1 "$2"
		[ $# -lt 3 ] || printf "%s\r\n" "$3"
		CGI_HDGEN=no # To prevent duplicated header.
	fi
}

ok() {
	printout 200 "OK" "$1=$2"
}

# Send error reply to post requests
# Must read file upload before sending
error_post() {
	cat >/dev/null
	printout 400 "$*"
	exit 1
}

error() {
	case "$1" in
		4??)
			printout $1 "$2"
			;
		*)
			printout 400 "$*"
			;
	esac

	exit 1
}

# @$1: set|cancel LED state
# @$2: LED state
dbus_set_led() {
	local cmd
	case $1 in
		set)
			cmd=SetState
			;
		cancel)
			cmd=CancelState
			;
		*)
			error "Bad command '$1' for LED state '$2'"
			;
	esac

	gdbus call -y -d com.axis.LEDController -o /com/axis/LEDController \
		-m com.axis.LEDController.$cmd "$2" >/dev/null 2>&1 || :
}

# @$1: play|record
# @$2: options for @$1 command
dbus_send_audio() {
	local method
	case $1 in
		play)
			method=StartPlayingClip
			;
		rec)
			method=RecordClip
			;
		*)
			;
	esac
	gdbus call -y --dest=com.axis.Streamer -o /com/axis/Streamer/Audio \
		-m com.axis.Streamer.Audio.$method "$2" 2>&1
}

# Remove file:// prefix if there
# if not http:// check so it starts with $AUDIO_FILE_PATH/
#                and not contain ..
sane_location() {
	LOCATION=${LOCATION#file://}
	if [ "${LOCATION##http://}" = "$LOCATION" ]; then
		[ "${LOCATION##$AUDIO_FILE_PATH/}" != "$LOCATION" ] || return 1
		case $LOCATION in
			*/../*|../*|*/..|..)
				return 2
				;
		esac
	fi
}

# @$1: action
check_num_clips() {
	local max_mediaclips nbr_of_groups errf=error

	[ "$1" != upload ] || errf=error_post

	max_mediaclips=$($PHC get MediaClip.MaxGroups - RAW) ||
		error "Maximum number of allowed audioclips not found in the '$1' action."

	nbr_of_groups=$($PHC getgrouplist MediaClip | wc -l)

	[ $nbr_of_groups -ne $max_mediaclips ] ||
		$errf "Failed to $1 more audioclips." \
		      "The maximum number of permitted audio clips" \
		      "'$max_mediaclips' has been reached."
}

# @$1: action
# @$2: value id
# @$3: value to test
is_integer() {
	case $3 in
		*[!0-9]*)
			error "Parameter '$2' must be an integer in the '$1' action."
			;
	esac
}

# Arg 1: group (A0, A1...)
has_audio_output() {
	local output

	[ $# -eq 1 ] && [ "$1" ] || return 1

	output=$($PHC get Properties.Audio.Source.$1.Output - RAW) ||
		error "Unable to fetch audio output $1"

	[ "$output" = yes ]
}

# $1 - variable name to return the source with
get_default_output_source() {
	local f=get_default_output_source sources source

	[ $# -eq 1 ] && [ "$1" ] || error "$f: missing argument"

	# Get available audio sources
	sources=$($PHC getgrouplist AudioSource - RAW) ||
		error "Unable to fetch audio sources"

	for source in $sources; do
		if has_audio_output $source; then
			eval $1=\$source
			return 0
		fi
	done

	error "No default audio output found"
}

play() {
	local clip_nr ret group_nbr default_source audiooutput

	clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
		error "Missing parameter: clip required in the 'play' action."
	is_integer play clip_nr "$clip_nr"

	LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW |
		   urldecode -e) ||
		error "Clip number '$clip_nr' not found in the 'play' action."

	audiooutput=$(__qs_getparam audiooutput) || audiooutput=
	if [ "$audiooutput" ]; then
		is_integer play audiooutput "$audiooutput"

		# Confirm that provided audio source exists and has output
		group_nbr=$(($audiooutput - 1))
		has_audio_output A$group_nbr ||
			error "No output for AudioSource.A$group_nbr"
	else
		# Get default AudioSource number with audio output, then increase by 1
		get_default_output_source default_source
		group_nbr=${default_source#?}
		audiooutput=$(($group_nbr + 1))
	fi
	ret=$(dbus_send_audio play "location=$LOCATION&audiooutput=$audiooutput") ||
		error "Failed to play clip '$clip_nr': '$ret'."
	ok "playing" $clip_nr
}

record() {
	local samplerate mediatype duration wait clip_nr ret name statusled cnt
	local channel

	samplerate=$(__qs_getparam samplerate) ||
		samplerate=$SAMPLE_RATE_DEFAULT
	case $samplerate in
		$SAMPLE_RATE_DEFAULT|8000)
			;
		*)
			error "Samplerate '$samplerate' not supported in the 'record' action."
			;
	esac

	mediatype=$(__qs_getparam media) && [ "$mediatype" ] ||
		error "Missing parameter: Media type required in the 'record' action."
	[ "$mediatype" = audio ] ||
		error "Unknown media type in the 'record' action." \
		      "Supported media types: audio"

	duration=$(__qs_getparam duration) || duration=10
	is_integer record duration "$duration"

	wait=$(__qs_getparam wait) || wait=0
	is_integer record wait "$wait"

	check_num_clips record

	clip_nr=$($PHC addgroup MediaClip mediaclip) ||
		error "Failed to add clip to parameters: '$clip_nr' in the 'record' action."
	LOCATION=$AUDIO_FILE_PATH/$clip_nr.au
	ret=$($PHC set $clip_nr.Location $LOCATION) ||
		error "Failed to set clip location: '$ret' in the 'record' action."
	name=$(__qs_getparam name) && [ "$name" ] ||
		name="Clip $clip_nr"
	ret=$($PHC set $clip_nr.Name "$name") ||
		error "Failed to set clip name: '$ret' in the 'record' action."
	statusled=$(__qs_getparam statusled) && [ "$statusled" ] ||
		statusled=yes
	channel=$(__qs_getparam audiochannel) || channel=1
	[ "$statusled" = no ] || dbus_set_led set LED_STATE_AUDIOCLIP_WAIT
	[ $wait -le 0 ] || sleep $wait
	ret=$(dbus_send_audio rec "location=$LOCATION&duration=$duration&audio=1&audiocodec=g711&audiosamplerate=$samplerate&audiochannel=$channel") ||
		error "Failed to record clip '$clip_nr': '$ret'."
	ok "recording" $clip_nr

	[ "$statusled" = no ] || dbus_set_led set LED_STATE_AUDIOCLIP_RECORD
	[ $duration -le 0 ] || {
		sleep $duration
		cnt=5
		while [ ! -f $LOCATION ] && [ $cnt -gt 0 ]; do
			cnt=$(($cnt - 1))
			sleep 1
		done
	}
	[ "$statusled" = no ] || {
		dbus_set_led cancel LED_STATE_AUDIOCLIP_RECORD
		dbus_set_led cancel LED_STATE_AUDIOCLIP_WAIT
	}
}

update() {
	local clip_nr clip_name

	clip_nr=$(__qs_getparam clip) ||
		error "Missing parameter: clip required in the 'update' action."
	is_integer update clip_nr "$clip_nr"

	clip_name=$(__qs_getparam name) ||
		error "Missing parameter: name required in the 'update' action."
	[ "$clip_name" ] || error "Clip name must not be blank."
	$PHC set MediaClip.M$clip_nr.Name "$clip_name" ||
		error "Failed to update clip '$clip_nr'."
	ok "updated" $clip_nr
}

remove() {
	local clip_nr

	clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
		error "Missing parameter: clip required in the 'remove' action."
	is_integer remove clip_nr "$clip_nr"

	LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW) ||
		error "Clip number '$clip_nr' not found in the 'remove' action."
	$PHC deletegroup MediaClip.M$clip_nr ||
		error "Failed to remove clip parameter 'M$clip_nr'."
	sane_location ||
		$LOGW "Clip location not valid, ignoring removal:" \
		      "'$LOCATION'"
	[ "${LOCATION##http://}" != "$LOCATION" ] ||
		rm -f "$LOCATION" ||
		$LOGW "Failed to remove clip location '$LOCATION': $?."
	ok "removed" $clip_nr
}

download() {
	local clip_nr mimetype

	clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
		error "Missing parameter: clip required in the 'download' action."
	is_integer download clip_nr "$clip_nr"

	LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW) ||
		error "Clip number '$clip_nr' not found in the 'download' action."
	sane_location || error "Clip location not valid in the 'download' action."
	[ -r "$LOCATION" ] ||
		error "Clip location '$LOCATION' not found in the 'download' action."
	if [ ${LOCATION##*.} = au ]; then
		mimetype=audio/basic
	else
		mimetype=audio/x-wav
	fi
	__cgi_hdgen yes "$(printf '%s\r\nContent-Disposition: attachment; filename="%s"' $mimetype "${LOCATION##*/}")"
	cat "$LOCATION" || exit 1
}

# @$1: file location
# @$2: error message
# @$3: error code
on_failed_file_operation() {
	[ $# -ge 1 ] && [ "$1" ] || error "Invalid file argument"

	[ ! -f "$1" ] || rm "$1" || error "Unable to remove file $1"

	if [ $# -gt 2 ]; then
		error $3 "$2"
	elif [ $# -eq 2 ]; then
		error "$2"
	else
		error "File operation failed"
	fi
}

upload() {
	local mediatype clip_nr ret name existing_param

	mediatype=$(__qs_getparam media) && [ "$mediatype" ] ||
		error_post "Missing parameter: media required in the 'upload' action."
	[ "$mediatype" = audio ] ||
		error_post "Unknown media type in the 'upload' action." \
			   "Supported media types: audio"

	check_num_clips upload

	LOCATION=$(file_upload -s $CONTENT_LENGTH -n 65536 -a \
			--allowed-realms= --allowed-dirs=audioclips --default-dir=audioclips) ||
		error 413 "Failed to upload clip. ($?) size=$CONTENT_LENGTH"

	chgrp streamer "$LOCATION" && chmod 640 "$LOCATION" ||
		on_failed_file_operation $LOCATION "Failed to change owner and/or " \
			"permissions on '$LOCATION' in the 'upload' action."

	audio_file is_supported_audio "$LOCATION" ||
		on_failed_file_operation $LOCATION "Unsupported Media Type" 415

	# Retrieve, if any: mediaclip.m[$clip_nr].Location=[$LOCATION]
	existing_param=$($PHC --nolog getgroup MediaClip 2>/dev/null | grep "$LOCATION") || :

	# If $LOCATION already exist in any MediaClip group
	if [ "$existing_param" ]; then
		# Extract and return existing parameter group number
		clip_nr=${existing_param#root.}
		clip_nr=${clip_nr%.Location=*}
		ok "updated" $clip_nr
	else
		clip_nr=$($PHC addgroup MediaClip mediaclip) ||
			on_failed_file_operation $LOCATION \
				"Failed to add clip to parameters: '$clip_nr' in the 'upload' action."

		ret=$($PHC set $clip_nr.Location "$LOCATION") ||
			on_failed_file_operation $LOCATION \
				"Failed to set clip location: '$ret' in the 'upload' action."

		name=$(__qs_getparam name) && [ "$name" ] ||
			name="Clip $clip_nr"

		ret=$($PHC set $clip_nr.Name "$name") ||
			on_failed_file_operation $LOCATION "Failed to set clip name: '$ret' " \
				"in the 'upload' action."
		ok "uploaded" $clip_nr
	fi
}

action=$(__qs_getparam action) && [ "$action" ] ||
	error "Missing parameter: action argument is required."

case $action in
	play|record|update|remove|download|upload)
		$action
		;
	*)
		error "Unknown action: '$action'." \
			"action argument is required."
		;
esac