diff --git a/mpd-monitor b/mpd-monitor index 09667685c2674a81d453619c2e0364f9a8dbcae7..081f35b7179a11ddffc69254faf95d2c1c88b678 100755 --- a/mpd-monitor +++ b/mpd-monitor @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1117 ## This script for bash 4.x on linux displays the way an audio file is ## streamed from storage, through mpd, to alsa (the DAC). ## @@ -22,7 +23,7 @@ LANG=C app_name_mm="mpd-monitor" -app_version="0.1.0" +app_version="0.1.1" app_info_url="https://lacocina.nl/mpd-display-status" def_mpd_host="localhost" @@ -44,6 +45,8 @@ arg_ssh_user="" arg_mpd_music_dir="" arg_mpd_password="" +skip_file_check="" + #PS4='+ $(date "+%s.%N")\011 ' #exec 3>&2 2>/tmp/bashstart.$$.log #set -x @@ -75,19 +78,107 @@ Optional arguments: Background information: ${app_info_url} + +${app_name_mm} ${app_version} " printf 1>&2 "%s\n" "${msg_usage}" } +function die() { + printf "error: %s\n" "$@" 1>&2 + exit 1 +} + +function debug() { + if [[ ${DEBUG} ]]; then + printf 1>&2 "${dim}[DEBUG] %s${std}: ${white}%s${std}\n" \ + "${FUNCNAME[1]}" "$@" + fi +} + +function display_error() { + printf 1>&2 "${red}${bold}Error in %s${std}: ${white}%s${std}\n\n" \ + "${FUNCNAME[1]}" "$@" + return 1 +} + +function display_usage_error() { + printf 1>&2 "${red}${bold}Usage error: ${std}${white}%s${std}\n\n" "$@" + display_usageinfo + return 1 +} + + +function bytes_to_si() { + ## see: http://www.pixelbeat.org/docs/numfmt.html + ## return the human notation for the number of bytes specified in $1. + ## strip decimals from input + bytes="${1}" + si_size="" + if [[ "${cmd_numfmt}x" != "x" ]]; then + #bytes="$(LC_ALL=nonexisting numfmt --from=si ${si_size})" + si_size="$(LC_ALL=nonexisting ${cmd_numfmt} --to=iec-i --suffix=B --format="%.1f" "${bytes}" 2>/dev/null)" + fi + if [[ "${si_size}x" == "x" ]]; then + echo TODO 1>&2 + return 1 + fi + printf "%s" "${si_size}" +} + + +function run_func() { + ## run an internal function whose name is specified in $1 with + ## args on the local host or remote through ssh. + func="$1" + shift + # shellcheck disable=SC2124 + args="$@" + debug "$(declare -p func args)" + if [[ "${alsa_ssh_userhost}x" == "x" ]]; then + "${func} ${args}" + else + ## use declare -f to first print the function in the remote + ## bash session, and then execute it. + # shellcheck disable=SC2086 + ssh "${alsa_ssh_userhost}" -- \ + "$(declare -f ${func}); ${func} ${args}" + fi +} + + +function get_csv_value() { + data="$1" + IFS=\; read -r type field value <<<"${data}" + printf "%s" "${value//\"}" +} + +function check_commands() { + declare -a err + msg="not found in PATH" + cmd_mpc=$(type -p mpc) || err+=("mpc") + cmd_soxi=$(type -p soxi) || err+=("soxi") + cmd_exiftool=$(type -p exiftool) || err+=("exiftool") + cmd_netcat=$(type -p nc) || err+=("netcat (nc)") + cmd_bc=$(type -p bc) || err+=("bc") + cmd_numfmt=$(type -p numfmt) + if [[ ${#err[@]} -gt 0 ]]; then + printf 1>&2 "required command \`%s' ${msg}\n" "${err[@]}" + return 1 + fi +} + + function analyze_command_line() { ## parse command line arguments using the `manual loop` method ## described in http://mywiki.wooledge.org/BashFAQ/035. while :; do case "${1:-}" in - -h|--mpd-host) + -m|--mpd-host) if [[ "${2}x" == "x" ]]; then - die "argument \`$1' requires the dns host name or ip address for the host running mpd." + display_error "argument \`$1' requires the dns host name or ip address for the host running mpd." + return 1 else arg_mpd_host="$2" shift 2 @@ -96,7 +187,8 @@ function analyze_command_line() { ;; -x|--mpd-password) if [[ "${2}x" == "x" ]]; then - die "argument \`$1' requires a non-empty value." + display_error "argument \`$1' requires a non-empty value." + return 1 else arg_mpd_password="${2}" shift 2 @@ -105,7 +197,8 @@ function analyze_command_line() { ;; -u|--ssh-user) if [[ "${2}x" == "x" ]]; then - die "argument \`$1' requires a non-empty value." + display_error "argument \`$1' requires a non-empty value." + return 1 else arg_ssh_user="${2}" shift 2 @@ -114,8 +207,8 @@ function analyze_command_line() { ;; -d|--mpd-music-dir) if [[ "${2}x" == "x" ]]; then - die "argument \`$1' requires the path to the directory where the music for mpd is stored." - break + display_error "argument \`$1' requires the path to the directory where the music for mpd is stored." + return 1 else arg_mpd_music_dir="$2" mpd_music_dir="${arg_mpd_music_dir}" @@ -123,7 +216,7 @@ function analyze_command_line() { continue fi ;; - -\?|--help) + -\?|-h|--help) display_usageinfo exit ;; @@ -147,21 +240,25 @@ function analyze_command_line() { #printf 1>&2 "OPTIND: \`%s'\n" "${OPTIND}" shift $((OPTIND - 1)) if [[ "${arg_mpd_music_dir}x" == "x" ]]; then + # shellcheck disable=SC2124 arg_single="$@" + #declare -p arg_single 1>&2 arg_mpd_music_dir="${arg_single}" - if [[ ! -d "${arg_mpd_music_dir}" ]]; then - printf 1>&2 "error: invalid mpd music directory \`%s' specified.\n\n" \ - "${arg_mpd_music_dir}" - display_usageinfo - else - ## single argument, use in defaults - mpd_music_dir="${arg_mpd_music_dir}" - mpd_host="${def_mpd_host}" - mpd_port="${def_mpd_port}" - mpd_password="" - ssh_host="" - fi - else + if [[ "${arg_mpd_music_dir}x" != "x" ]]; then + + if [[ ! -d "${arg_mpd_music_dir}" ]]; then + display_usage_error "invalid mpd music directory \`${arg_mpd_music_dir}' specified." + else + ## single argument, use in defaults + mpd_music_dir="${arg_mpd_music_dir}" + mpd_host="${def_mpd_host}" + mpd_port="${def_mpd_port}" + mpd_password="" + ssh_host="" + fi + else + display_usage_error "no mpd music directory specified." + fi [[ ${DEBUG} ]] && \ declare -p arg_mpd_music_dir \ arg_mpd_host arg_mpd_port arg_mpd_password \ @@ -170,87 +267,6 @@ function analyze_command_line() { } -function die() { - printf "error: %s\n" "$@" 1>&2 - exit 1 -} - -function bytes_to_si() { - ## see: http://www.pixelbeat.org/docs/numfmt.html - ## return the human notation for the number of bytes specified in $1. - ## strip decimals from input - bytes="${1}" - si_size="" - if [[ "${cmd_numfmt}x" != "x" ]]; then - #bytes="$(LC_ALL=nonexisting numfmt --from=si ${si_size})" - si_size="$(LC_ALL=nonexisting ${cmd_numfmt} --to=iec-i --suffix=B --format="%.1f" "${bytes}" 2>/dev/null)" - fi - if [[ "${si_size}x" == "x" ]]; then - echo TODO 1>&2 - return 1 - fi - printf "%s" "${si_size}" -} - - -function run_func() { - ## run an internal function whose name is specified in $1 with - ## args on the local host or remote through ssh. - func="$1" - shift - args="$@" - [[ ${DEBUG} ]] && \ - declare -p 1>&2 func args - if [[ "${alsa_ssh_userhost}x" == "x" ]]; then - "${func} ${args}" - else - ## use declare -f to first print the function in the remote - ## bash session, and then execute it. - ssh ${alsa_ssh_userhost} -- \ - "$(declare -f ${func}); ${func} ${args}" - fi -} - - -function check_commands() { - declare -a err - msg="not found in PATH" - cmd_mpc=$(type -p mpc) || err+=("mpc") - cmd_soxi=$(type -p soxi) || err+=("soxi") - cmd_exiftool=$(type -p exiftool) || err+=("exiftool") - cmd_netcat=$(type -p nc) || err+=("netcat (nc)") - cmd_bc=$(type -p bc) || err+=("bc") - cmd_numfmt=$(type -p numfmt) - if [[ ${#err[@]} -gt 0 ]]; then - printf 1>&2 "required command \`%s' ${msg}\n" "${err[@]}" - return 1 - fi -} - -function get_csv_value() { - data="$1" - IFS=\; read type field value <<<"${data}" - printf "%s" "${value//\"}" -} - -function get_soxi_value() { - fieldname="$1" - printf 1>&2 "%s: fieldname='%s', value='%s'\n" \ - "${FUNCNAME[0]}" "${fieldname}" "${soxi_info["${fieldname}"]}" - declare vals="${soxi_info["${fieldname}"]}" - #declare -p vals 1>&2 - value="$(get_csv_value "${vals[0]}")" - printf "%s" "${value}" -} - -function filter_soxi_duration() { - ## expects: hh:mm:ss.xxx where xxx are the subseconds (milliseconds?) - duration_fields=($1) - IFS=":" read hours minutes seconds subseconds <<<"${duration_fields[0]//./:}" - minutes=$(${cmd_bc} <<< "${minutes} + ( ${hours} * 60 )") - printf "%s:%s" "${minutes}" "${seconds}" -} - function ret_mpdconf_contents() { ## return the contents of the mpd configuration file specified in @@ -271,7 +287,7 @@ function ret_mpdconf_commandline() { ## return the command line arguments for running mpd alsa_ssh_userhost="${alsa_ssh_userhost:-}" mpd_pid="$(pidof mpd | awk '{print $1}')" - xargs -0 echo < /proc/${mpd_pid}/cmdline + xargs -0 echo < "/proc/${mpd_pid}/cmdline" } @@ -286,7 +302,7 @@ function ret_mpdconf_default() { [[ ${DEBUG} ]] && \ printf 1>&2 "using mpd configuration file %s\n" \ "${conffile}" - echo ${conffile} + echo "${conffile}" return 0 fi fi @@ -309,18 +325,16 @@ function ret_mpdconf_path() { mpd_args+=(--stderr) ## strip each command line argument from all possible arguments ## the remainder is the mpd conf file + # shellcheck disable=SC2207 mpd_real_args=($(run_func ret_mpdconf_commandline)) - mpd_rest_args=(${mpd_real_args[@]:1}) - for arg in ${mpd_args[@]}; do - mpd_rest_args=(${mpd_rest_args[@]//${arg}}) + mpd_rest_args=("${mpd_real_args[@]:1}") + for arg in "${mpd_args[@]}"; do + mpd_rest_args=("${mpd_rest_args[@]//${arg}}") done - [[ ${DEBUG} ]] && \ - declare -p mpd_args mpd_real_args mpd_rest_args 1>&2 + debug "$(declare -p mpd_args mpd_real_args mpd_rest_args)" mpd_conf_file="${mpd_rest_args[0]}" if [[ "${mpd_conf_file}x" == "x" ]]; then - [[ ${DEBUG} ]] && \ - printf 1>&2 "no conffile specified on mpd command line, \ -test default locations\n" + debug "no conffile specified on mpd command line, test default locations." run_func ret_mpdconf_default || return 1 else printf "%s" "${mpd_conf_file}" @@ -340,7 +354,7 @@ function get_mpd_alsa_outputs() { # while read -r line; do # done< <(ssh ${alsa_ssh_userhost} -- "cat /ec/mpd.conf") mpdconf_path=$(ret_mpdconf_path) - [[ ${DEBUG} ]] && printf 1>&2 "%s: mpdconf_path=\`%s'\n" "${FUNCNAME}" "${mpdconf_path}" + debug "$(declare -p mpdconf_path)" #mpdconf_contents="$(get_mpdconf_contents "${mpdconf_path}")" #declare -p mpdconf_path mpdconf_contents 1>&2 start_audio_output_re="^[[:space:]]*audio_output[[:space:]]*\{" @@ -391,29 +405,77 @@ function get_mpd_alsa_outputs() { [[ ${DEBUG} ]] && \ declare -p audio_outputs devices 1>&2 ## TODO: handle multiple devices - for device in ${devices[@]}; do + for device in "${devices[@]}"; do printf "%s\n" "${device}" done } +function check_input_type() { + mpc_vals_file="$1" + uri_spec="${mpc_vals_file//:*/}" + debug "checking input type for \`${mpc_vals_file}' with uri spec \`${uri_spec}'." + if [[ "${mpc_vals_file}x" != "${uri_spec}x" ]]; then + debug "input type is not a file (${uri_spec})." + return 2 + else + debug "input type is a file." + if [[ "${mpc_vals_file}x" == "x" ]]; then + display_error "empty mpc_vals_file." + return 2 + else + rel_file_path="${arg_mpd_music_dir}/${mpc_vals[file]}" + if [[ ! -f "${rel_file_path}" ]]; then + display_error "invalid or non-existing file \`${mpc_vals_file}'." + return 1 + else + debug "using input file \`${rel_file_path}'." + return 0 + fi + fi + fi +} + + function fill_arrays() { - ret_mpc_info || return 1 + + if ret_mpc_info; then + debug "ret_mpc_info success." + else + debug "ret_mpc_info error." + return 1 + fi #ret_mpc_now - ret_mpd_now || return 1 - ret_exif_info "${mpc_vals[file]}" - ret_soxi_info "${mpc_vals[file]}" + if ! ret_mpd_now; then + display_error "ret_mpd_now returned an error" + return 1 + fi + check_input_type "${mpc_vals[file]}" + case $? in + 1) + return 1 + ;; + 2) + skip_file_check=true + ;; + *) + rel_file_path="${arg_mpd_music_dir}/${mpc_vals[file]}" + if ! ret_exif_info "${mpc_vals[file]}"; then + display_error "ret_exif_info returned an error" + return 1 + fi + if ! ret_soxi_info "${mpc_vals[file]}"; then + display_error "ret_soxi_info returned an error" + return 1 + fi + debug "ret_exif_info and ret_soxi_info ok." + ;; + esac ## TODO get right card and interface numbers alsa_outputs=("$(get_mpd_alsa_outputs)") - [[ ${DEBUG} ]] && declare -p alsa_outputs 1>&2 - #for output in ${alsa_outputs[0]}; do -# ret_alsa_info "${output}" - # done - for output in ${alsa_outputs[@]}; do + debug "$(declare -p alsa_outputs)" + for output in "${alsa_outputs[@]}"; do ret_alsa_info "${output}" done - #[[ "${exif_vals[*]}x" == "x" ]] || ret_exif_info "${mpc_vals[file]}" - #[[ "${soxi_vals[*]}x" == "x" ]] || ret_soxi_info "${mpc_vals[file]}" - #[[ "${alsa_vals[*]}x" == "x" ]] || ret_alsa_info "1" "0" "root@sonida" } @@ -436,153 +498,12 @@ function alsa_format_outputencoding() { esac else printf "ANOMALITY in %s: alsa_encoding '%s' does not match regex.\n" \ - "${FUNCNAME[0]}" "${alsa_encoding}" + "${FUNCNAME[0]}" "${alsa_encoding[*]}" return 1 fi } -function get_info() { - - ## check presence of needed commands - check_commands || die "not all needed commands are installed." - - ## parse command line arguments - analyze_command_line "$@" - - if [[ ! -d "${mpd_music_dir}" ]]; then - die "error: can't access directory ${mpd_music_dir}." - fi - mpd_host="${arg_mpd_host:-${def_mpd_host}}" - mpd_port="${arg_mpd_port:-${def_mpd_port}}" - ssh_user="${arg_ssh_user:-${def_ssh_user}}" - mpd_password="${arg_mpd_password:-}" - ssh_host="${mpd_host:-}" - localhost_re="127.0.[0-9]+.[0-9]+|localhost" - if [[ "${mpd_host}" =~ ${localhost_re} ]]; then - ## mpd runs local, no need for ssh to get alsa properties - alsa_ssh_userhost="" - else - alsa_ssh_host="${mpd_host}" - ## mpd runs remote, use ssh to get alsa properties - alsa_ssh_userhost="${ssh_user}@${ssh_host}" - fi - [[ ${DEBUG} ]] && \ - declare -p mpd_host ssh_host ssh_user mpd_music_dir 1>&2 - - fill_arrays - [[ ${DEBUG} ]] && \ - declare -p mpc_vals mpd_vals 1>&2 - - main_playing_tracktitle="${mpc_vals[title]}" - main_playing_trackartist="${mpc_vals[artist]}" - main_playing_trackperformers="${exif_vals[Performer]}" - main_rt_tracknumber_album="${mpc_vals[track]}" - main_rt_tracknumber_playlist="${mpc_vals[position]}" - main_rt_playlistlength="${mpd_vals[rt_raw_playlistlength]}" - main_playing_trackalbumname="${mpc_vals[album]}" - main_playing_trackalbumnumber="${mpc_vals[position]}" - main_playing_trackalbumartist="${mpc_vals[albumartist]}" - main_playing_trackduration="$(filter_soxi_duration "${soxi_vals[Duration]}")" - [[ $? -ne 0 ]] && return 1 - file_path="${arg_mpd_music_dir}/${mpc_vals[file]}" - if [[ ! -f "${file_path}" ]]; then - return 1 - ## TODO - fi - a_du_output=($(du --bytes --dereference "${file_path}")) - main_file_sizebytes="${a_du_output[0]}" - ## TODO: declare -p main_file_sizebytes 1>&2 - main_file_sizehbytes="$(bytes_to_si "${main_file_sizebytes}")" - [[ $? -ne 0 ]] && return 1 - msg_filesize="$(printf "%-10s: %-13s (%s)" \ -"File size" "${main_file_sizebytes} bytes" "${main_file_sizehbytes}")" - - main_file_bitrate_raw="${soxi_vals[BitRate]}" - main_file_bitrate_value="${main_file_bitrate_raw:0:-1}" - main_file_bitrate_unit="${main_file_bitrate_raw:$(( ${#main_file_bitrate_raw} - 1 )):1}" - main_file_bitrate_kvalue= - if [[ "${main_file_bitrate_unit}x" == "Mx" ]]; then - int="${main_file_bitrate_value%%.*}" - dec="${main_file_bitrate_value##*.}" - ## TODO - [[ ${DEBUG} ]] && \ - declare -p main_file_bitrate_value int dec 1>&2 - main_file_bitrate_kvalue=$(bc <<< "${main_file_bitrate_value} * 1000") - else - main_file_bitrate_kvalue="${main_file_bitrate_value}" - fi - main_file_bitrate_kvalue="${main_file_bitrate_kvalue%%.00}" - main_file_bitrate="${main_file_bitrate_kvalue}" - main_file_bitdepth="${soxi_vals[Precision]//-bit}" - main_file_samplerate="${soxi_vals[SampleRate]}" - #declare -p alsa_vals 1>&2 - main_output_samplerate_raw=(${alsa_vals[rate]}) - main_output_samplerate="${main_output_samplerate_raw[0]}" - ## get and store alsa output encoding - alsa_format_outputencoding "${alsa_vals[format]}" - main_output_bitdepth="${alsa_encoding[bitdepth]}" - main_output_sampleencoding="${alsa_encoding[bitdepth]} bit (${alsa_encoding[signedness]} ${alsa_encoding[endianness]})" - main_output_samplechannelcount="${alsa_vals[channels]}" - - main_playing_trackplayingtime="${mpc_vals[rt_time_elapsed_minutes]}:${mpc_vals[rt_time_elapsed_seconds]}" - main_playing_trackplayingpercentage="${mpc_vals[rt_percentage_played]}%" - #declare -p main_playing_trackplayingpercentage 1>&2 - main_mpd_bitdepth="${mpd_vals[rt_audio_bitdepth]}" - main_mpd_samplerate="${mpd_vals[rt_audio_samplerate]}" - main_mpd_bitrate="${mpd_vals[rt_audio_bitrate]}" - - ## determine if playback is bit perfect - color_bitdepth_file_mpd="${red}" - color_bitdepth_mpd_output="${red}" - if [[ ${main_mpd_bitdepth} -ge ${main_file_bitdepth} ]]; then - color_bitdepth_file_mpd="${green}" - if [[ ${main_output_bitdepth} -ge ${main_mpd_bitdepth} ]]; then - color_bitdepth_mpd_output="${green}" - fi - fi - color_samplerate_file_mpd="${red}" - color_samplerate_mpd_output="${red}" - if [[ ${main_mpd_samplerate} -ge ${main_file_samplerate} ]]; then - color_samplerate_file_mpd="${green}" - if [[ ${main_output_samplerate} -ge ${main_mpd_samplerate} ]]; then - color_samplerate_mpd_output="${green}" - fi - fi - - ## prepare output - msg_header="$(printf "%-11s %-18s ${white}%8s${std} ${white}%8s${std} ${white}%8s${std}" " " " " "${label_input}" "${label_throughput}" "${label_output}")" - msg_bitrate="$(printf "%-11s %-18s: ${dim}${white}%8s${std} > ${white}%8s${std} > ${dim}${white}%8s${std}" \ -" " "${label_bitrate}" "${main_file_bitrate}" "${main_mpd_bitrate}" "(n/a)")" - msg_bitdepth="$(printf "%-11s %-18s: %8s ${color_bitdepth_file_mpd}> %8s ${color_bitdepth_mpd_output}> %8s${std}" \ -" " "${label_bitdepth}" "${main_file_bitdepth}" "${main_mpd_bitdepth}" "${main_output_bitdepth}")" - msg_samplerate="$(printf "%-11s %-18s: %8s ${color_samplerate_file_mpd}> %8s${std} ${color_samplerate_mpd_output}> %8s${std}" \ -" " "${label_samplerate}" "${main_file_samplerate}" "${main_mpd_samplerate}" "${main_output_samplerate}")" - - msg_tracktitle="${bold}${white}${main_playing_tracktitle}${std}" - msg_trackartist="${blue}${main_playing_trackartist}${std}" - msg_tracknumber="$(printf "%-4s of %-3s" "#${bold}${blue}${main_rt_tracknumber_playlist}${std}" "${main_rt_playlistlength}")" - msg_nowplaying="${msg_tracknumber}: ${msg_tracktitle}" - msg_byline="$(printf "%-11sby %s" " " "${msg_trackartist}")" - msg_albumline="$(printf "%-11strack %s from album %s" \ -" " "${bold}${main_rt_tracknumber_album}${std}" "${bold}${blue}${main_playing_trackalbumname}${std}")" - msg_playingtime="$(printf "%-13s playing %s from %s (%s)" \ -" " "${bold}${std}${main_playing_trackplayingtime}${std}" "${main_playing_trackduration}" "${main_playing_trackplayingpercentage}")" - - echo -en "\ -${msg_playingtime} -${msg_nowplaying} -${msg_albumline} -${msg_byline} - -${msg_header} -${msg_bitdepth} -${msg_samplerate} -${msg_bitrate} - -${msg_filesize} -\r" -} function ret_formatted_time() { ## returns formatted time (hh:mm:ss) from number of seconds ($1) @@ -595,38 +516,48 @@ function ret_formatted_time() { "${hours}" "${minutes}" "${seconds}" } -function ret_mpd_now() { +function get_nc_args() { ## echo status command to mpd host on mpd port using netcat, to ## fill mpc_vals array with rt_ fields - - [[ ${DEBUG} ]] && \ - declare -p cmd_netcat mpd_host mpd_port 1>&2 - res=$(${cmd_netcat} --help 2>&1 >/dev/null) + declare -a netcat_args + netcat_args=(--help) + debug "$(declare -p cmd_netcat mpd_host mpd_port)" + res=$(${cmd_netcat} "${netcat_args[@]}" 2>&1 >/dev/null) + # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then netcat_args=(-N) + debug "using netcat \`${cmd_netcat} with short args (${netcat_args[*]})." else netcat_args=(--close) + debug "using netcat \`${cmd_netcat} with long args (${netcat_args[*]})." fi - ## get realtime mpd status using netcat - mpd_status="$(${cmd_netcat} ${netcat_args[@]} ${mpd_host} ${mpd_port} <<< "status")" - [[ ${DEBUG} ]] && \ - declare -p mpd_status 1>&2 - linecounter=1 + ## return command + printf '%s' "${netcat_args[@]}" + +} + +function ret_mpd_now() { + ## echo status command to mpd host on mpd port using netcat, to + ## fill mpc_vals array with rt_ fields prefix="rt_raw" key_val_re="^([^\:]+):[[:space:]]+(.*)" ## store each line in an associative array, by splitting the mpc ## output lines (paramater:value) + # shellcheck disable=SC2207 + netcat_args=($(get_nc_args)) + debug "$(declare -p netcat_args)" while read -r line; do if [[ "${line}" =~ ${key_val_re} ]]; then field="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" mpd_vals["${prefix}_${field}"]="${value}" fi - done<<<"${mpd_status}" + done< <(${cmd_netcat} "${netcat_args[@]}" ${mpd_host} ${mpd_port} <<< 'status') + debug "$(declare -p mpd_vals)" ## store the double colon separated values ## (samplerate:bitdepth:channelcount, eg. 48000:24:2) of the ## audiofield in separate variables and an array - IFS=: read audio_samplerate audio_bitdepth audio_channelcount <<< "${mpd_vals[${prefix}_audio]}" + IFS=: read -r audio_samplerate audio_bitdepth audio_channelcount <<< "${mpd_vals[${prefix}_audio]}" mpd_vals["rt_audio_samplerate"]="${audio_samplerate}" mpd_vals["rt_audio_bitdepth"]="${audio_bitdepth}" mpd_vals["rt_audio_channelcount"]="${audio_channelcount}" @@ -634,25 +565,36 @@ function ret_mpd_now() { ## store the (elapsed) playing time value (seconds:frames, ## eg. 20:123) in separate variables and an array - IFS=: read time_elapsed_seconds time_duration_seconds <<< "${mpd_vals[${prefix}_time]}" - mpd_vals["rt_time_elapsed_seconds"]="${time_elapsed_seconds}" - mpd_vals["rt_time_duration_seconds"]="${time_duration_seconds}" - mpd_vals["rt_time_elapsed_formatted"]="$(ret_formatted_time "${time_elapsed_seconds}")" - mpd_vals["rt_time_duration_formatted"]="$(ret_formatted_time "${time_duration_seconds}")" - #mpd_vals["rt_time_tracktotal"]="${mpd_vals[${prefix}_tracktotal]}" - ## old - #mpd_val rt_time_tracktotal: '223' - #mpd_val rt_raw_time: '53:223' - #mpd_val rt_raw_elapsed: '52.911' - #mpd_val rt_raw_duration: '223.168' - - if [[ ${DEBUG} ]]; then - for k in "${!mpd_vals[@]}"; do - if [[ "${k}" =~ ^rt ]]; then - printf 1>&2 "mpd_val %s: '%s'\n" "${k}" "${mpd_vals[${k}]}" - fi - done - fi + IFS=: read -r mpdval_time_elapsed_seconds mpdval_time_duration_seconds <<< "${mpd_vals[${prefix}_time]}" + mpd_vals["rt_mpdval_time_elapsed_seconds"]="${mpdval_time_elapsed_seconds}" + mpd_vals["rt_mpdval_time_duration_seconds"]="${mpdval_time_duration_seconds}" + mpd_vals["rt_time_elapsed_formatted"]=$( + ret_formatted_time "${mpdval_time_elapsed_seconds}" + ) + mpd_vals["rt_time_duration_formatted"]=$( + ret_formatted_time "${mpdval_time_duration_seconds}" + ) + mpd_vals["rt_percentage_played"]=$(( 200 * mpdval_time_elapsed_seconds / mpdval_time_duration_seconds % 2 + 100 * mpdval_time_elapsed_seconds / mpdval_time_duration_seconds )) + debug "$(declare -p mpd_vals)" +} + +function run_mpc() { + mpc_args=("$@") + arg_waittime_ms="${arg_waittime_ms:-0.5}" + pidfile="$(mktemp "/tmp/${app_name_mm}.XXXXX.pid")" + ${cmd_mpc} "${mpc_args[@]}" 2>&1 & + pid="$!" + echo "${pid}" > "${pidfile}" + ( + sleep "${arg_waittime_ms}" + if [[ -e "${pidfile}" ]]; then + kill -9 "${pid}" + fi + ) & + killerpid="$!" + wait "${pid}" + kill -9 "${killerpid}" + rm -f "${pidfile}" 2>/dev/null } function ret_mpc_info() { @@ -666,18 +608,28 @@ function ret_mpc_info() { mpc_args+=(-p "${mpd_port}") mpc_args+=(current) mpc_args+=(-f "${mpc_format_string}") - [[ ${DEBUG} ]] && \ - declare -p mpc_format_string 1>&2 - res="$(${cmd_mpc} "${mpc_args[@]}" &)" - if [[ $? -ne 0 ]]; then - die "${FUNCNAME[0]}: mpd on %s is not running." - else - if [[ "${res}x" == "x" ]]; then - printf 1>&2 "mpd on %s is not currently playing anything.\n" \ - "${mpd_host}" + debug "$(declare -p mpc_format_string)" + res="$(run_mpc "${mpc_args[@]}")" + debug "$(declare -p res)" + case "${res}" in + "mpd error: Failed to resolve host name") + display_error "specified mpd host \`${mpd_host}' can't be reached." + return 2 + ;; + "TODO: i don't know") + display_error "mpd on \`${mpd_host}' is not currently playing anything." return 1 - fi - fi + ;; + "mpd error:*") + display_error "mpd on host \`${mpd_host}' returned error \`${res}'." + return 1 + ;; + "") + display_error "mpd on \`${mpd_host}' is not currently playing anything." + return 1 + ;; + esac + #return 1 field_val_re="^([^\:]+):(.*)" while read -r line; do if [[ "${line}" =~ ${field_val_re} ]]; then @@ -686,13 +638,17 @@ function ret_mpc_info() { mpc_vals["${field}"]="${val}" fi done<<<"${res}" + debug "$(declare -p mpc_vals)" } function ret_soxi_info() { rel_file_path="${1}" file_path="${arg_mpd_music_dir}/${rel_file_path}" - if [[ -f "${file_path}" ]]; then - res="$(${cmd_soxi} "${file_path}")" + if [[ ! -f "${file_path}" ]]; then + display_error "file \`${file_path}' not accessible or non-existant." + return 1 + else + res="$()" field_val_re='([^\:]+):[[:space:]]*(.*)' while read -r line; do if [[ "${line}" =~ ${field_val_re} ]]; then @@ -704,15 +660,24 @@ function ret_soxi_info() { break fi fi - done<<<"${res}" + done< <(${cmd_soxi} "${file_path}") + fi + if (( "${#soxi_vals[@]}" > 0 )); then + debug "$(declare -p soxi_vals)" + else + display_error "${cmd_soxi} \"${file_path}\" returned nothing." + return 1 fi } function ret_exif_info() { - rel_file_path="${1}" + rel_file_path="$1" file_path="${arg_mpd_music_dir}/${rel_file_path}" - if [[ -f "${file_path}" ]]; then - res="$(${cmd_exiftool} -j "${file_path}")" + if [[ ! -f "${file_path}" ]]; then + display_error "invalid or non-existing file \`${file_path}'." + return 1 + else + res="$()" field_val_re='"([^\:]+)":(.*)' while read -r line; do if [[ "${line}" =~ ${field_val_re} ]]; then @@ -720,7 +685,13 @@ function ret_exif_info() { val="${BASH_REMATCH[2]}" exif_vals["${field}"]="${val}" fi - done<<<"${res}" + done< <(${cmd_exiftool} -j "${file_path}") + fi + if (( "${#exif_vals[@]}" > 0 )); then + debug "$(declare -p exif_vals)" + else + display_error "${cmd_exiftool} -j \"${file_path}\" returned nothing." + return 1 fi } @@ -755,8 +726,7 @@ function ret_alsa_info() { alsa_ssh_userhost="${alsa_ssh_userhost:-}" unset mpd_outputs declare -a mpd_outputs - [[ ${DEBUG} ]] && \ - declare -p alsa_hw alsa_card_no alsa_dev_no alsa_ssh_userhost + debug "$(declare -p alsa_hw alsa_card_no alsa_dev_no alsa_ssh_userhost)" declare -a mpc_args if [[ "${mpd_password}x" == "x" ]]; then mpc_args=(-h "${mpd_host}") @@ -764,55 +734,235 @@ function ret_alsa_info() { mpc_args=(-h "${mpd_password}@${mpd_host}") fi mpc_args+=(-p "${mpd_port}") - mpc_args+=(output) - [[ ${DEBUG} ]] && \ - declare -p mpd_host mpd_port mpc_args 1>&2 + mpc_args+=(outputs) + debug "$(declare -p mpd_host mpd_port mpc_args)" mpc_output_re="Output[[:space:]]([0-9]+)[[:space:]]\(([^\(]+)\)" while read -r line; do if [[ "${line}" =~ ${mpc_output_re} ]]; then mpd_outputs+=("${BASH_REMATCH[2]}") fi done< <(${cmd_mpc} "${mpc_args[@]}") - [[ ${DEBUG} ]] && \ - declare -p mpd_outputs 1>&2 + debug "$(declare -p mpd_outputs)" key_val_re="^([^\:]+):[[:space:]]+(.*)" + debug "matching with key_val_re \`${key_val_re}':" while read -r line; do if [[ "${line}" =~ ${key_val_re} ]]; then field="${BASH_REMATCH[1]}" val="${BASH_REMATCH[2]}" alsa_vals["${field}"]="${val}" - [[ ${DEBUG} ]] && \ - declare -p line field val 1>&2 else - [[ ${DEBUG} ]] && \ - declare -p line 1>&2 - return 1 + debug "no match: $(declare -p line)" fi done< <(run_func get_hwparams "${alsa_card_no} ${alsa_dev_no}") + if (( ${#alsa_vals[@]} < 1 )); then + display_error "empty alsa_vals array." + return 1 + else + debug "$(declare -p alsa_vals)" + fi } -function terminal_size() { # Calculate the size of the terminal - terminal_cols="$(tput cols)" - terminal_rows="$(tput lines)" +function get_info() { + + ## check presence of needed commands + if check_commands; then + debug "all needed commands are installed." + else + display_error "not all needed commands are installed." + fi + + ## parse command line arguments + if analyze_command_line "$@"; then + debug "analyze_command_line \"$*\" passed." + else + display_error "analyze_command_line returned an error." + return 1 + fi + if [[ ! -d "${mpd_music_dir}" ]]; then + die "error: can't access directory ${mpd_music_dir}." + else + debug "$(declare -p mpd_music_dir)" + fi + mpd_host="${arg_mpd_host:-${def_mpd_host}}" + mpd_port="${arg_mpd_port:-${def_mpd_port}}" + ssh_user="${arg_ssh_user:-${def_ssh_user}}" + mpd_password="${arg_mpd_password:-}" + ssh_host="${mpd_host:-}" + localhost_re="127.0.[0-9]+.[0-9]+|localhost" + if [[ "${mpd_host}" =~ ${localhost_re} ]]; then + ## mpd runs local, no need for ssh to get alsa properties + alsa_ssh_userhost="" + else + ## mpd runs remote, use ssh to get alsa properties + alsa_ssh_userhost="${ssh_user}@${ssh_host}" + fi + debug "$(declare -p mpd_host ssh_host ssh_user mpd_music_dir)" + + if fill_arrays; then + debug "arrays filled: $(declare -p mpc_vals mpd_vals)." + else + display_error "unable to fill_arrays." + return 1 + fi + if [[ "${skip_file_check}x" != "x" ]]; then + debug "skipping file checks." + else + file_path="${arg_mpd_music_dir}/${mpc_vals[file]}" + # shellcheck disable=SC2207 + a_du_output=($(du --bytes --dereference "${file_path}")) + input_sizebytes="${a_du_output[0]}" + ## TODO: declare -p input_sizebytes 1>&2 + input_sizehbytes="$(bytes_to_si "${input_sizebytes}")" + # shellcheck disable=SC2181 + [[ $? -ne 0 ]] && return 1 + input_bitdepth="${soxi_vals[Precision]//-bit}" + input_bitrate_bit_s="$(( input_bitdepth * soxi_vals[SampleRate] * soxi_vals[Channels] ))" + input_bitrate_kbit_s=$(${cmd_bc} <<< "scale=0; ${input_bitrate_bit_s} / 1000") + #declare -p alsa_vals 1>&2 + fi + # shellcheck disable=SC2206 + output_samplerate_raw=(${alsa_vals[rate]}) + output_samplerate="${output_samplerate_raw[0]}" + debug "$(declare -p output_samplerate_raw output_samplerate)" + ## get and store alsa output encoding + alsa_format_outputencoding "${alsa_vals[format]}" + output_bitdepth="${alsa_encoding[bitdepth]}" + + mpd_bitdepth="${mpd_vals[rt_audio_bitdepth]}" + mpd_samplerate="${mpd_vals[rt_audio_samplerate]}" + mpd_bitrate="${mpd_vals[rt_audio_bitrate]}" + output_bitrate="$(( output_samplerate * output_bitdepth * 2 / 1000 ))" + ## determine if playback is bit perfect + color_bitdepth_file_mpd="${red}" + color_bitdepth_mpd_output="${red}" + if (( input_bitdepth > 0 )); then + if (( mpd_bitdepth >= input_bitdepth )); then + color_bitdepth_file_mpd="${green}" + if (( output_bitdepth >= mpd_bitdepth )); then + color_bitdepth_mpd_output="${green}" + fi + fi + fi + color_samplerate_file_mpd="${red}" + color_samplerate_mpd_output="${red}" + if (( soxi_vals[SampleRate] > 0 )); then + if (( mpd_samplerate >= soxi_vals[SampleRate] )); then + color_samplerate_file_mpd="${green}" + if (( output_samplerate >= mpd_samplerate )); then + color_samplerate_mpd_output="${green}" + fi + fi + fi + + ## prepare output + display_trackdetails + + echo -en "\ +${msg_nowplaying} +${msg_albumline} +${msg_filesize} +\r" + display_headerrow \ + "${label_input}" \ + "${label_throughput}" \ + "${label_output}" + display_column \ + "${label_samplerate}" \ + "${soxi_vals[SampleRate]}" \ + "${mpd_samplerate}" \ + "${color_samplerate_file_mpd}" \ + "${output_samplerate}" \ + "${color_samplerate_mpd_output}" + display_column \ + "${label_bitdepth}" \ + "${input_bitdepth}" \ + "${mpd_bitdepth}" \ + "${color_bitdepth_file_mpd}" \ + "${output_bitdepth}" \ + "${color_bitdepth_mpd_output}" + display_column \ + "${label_bitrate}" \ + "${input_bitrate_kbit_s}" \ + "${mpd_bitrate}" \ + "${color_bitdepth_file_mpd}" \ + "${output_bitrate}" \ + "${color_bitdepth_mpd_output}" } +function display_trackdetails() { + #${mpc_vals[position]} + #${mpd_vals[rt_raw_playlistlength]} -function main_loop() { - ## unused - mpc_args=(-h "${mpd_host}") - counter=0 - while : ; do - #"${cmd_mpc}" "${mpc_args[@]}" idle - sleep 0.5 - get_info || return 1 - fill_arrays - # \r" - ((counter++)) - echo -en "bla die bla $counter\ntwee bla die bla $counter\r" + if [[ "${skip_file_check}x" != "x" ]]; then + if [[ "${mpc_vals[title]}x" == "x" ]]; then + printf -v msg_nowplaying "Playing ${bold}${white}%s${std}" \ + "${mpc_vals[file]}" + else + printf -v msg_nowplaying "Playing ${bold}${white}%s${std}" \ + "${mpc_vals[title]}" + fi + printf -v msg_albumline "%-11sUri: ${bold}%s${std}" \ + " " \ + "${mpc_vals[file]}" + msg_playingtime="n/a" - #tclsh /etc/shairport/smartie/smartie-cat.tcl -tty /dev/ttyUSB0 - done - printf 1>&2 "main_loop done.\n" + else + printf -v msg_playingtime "%s / %s (%s)" \ + "${mpd_vals[rt_time_elapsed_formatted]}" \ + "${mpd_vals[rt_time_duration_formatted]}" \ + "${mpd_vals[rt_percentage_played]}%" + if [[ "${mpc_vals[title]}x" == "x" ]]; then + printf -v msg_nowplaying "Playing ${bold}${white}%s${std}" \ + "${mpc_vals[file]}" + else + printf -v msg_nowplaying "Playing ${bold}${white}%s${std} by ${blue}%s${std}" \ + "${mpc_vals[title]}" \ + "${mpc_vals[artist]}" + fi + msg_filesize="$(printf "%-11sFile size: %-13s (%s)" \ + " " \ + "${input_sizebytes} bytes" \ + "${input_sizehbytes}")" + if (( mpc_vals[track] > 0 )); then + printf -v msg_albumline "%-11sTrack ${bold}%s${std} " \ + " " \ + "${mpc_vals[track]}" + if [[ "${mpc_vals[album]}x" != "x" ]]; then + msg_albumline="${msg_albumline} from album ${bold}${blue}${mpc_vals[album]}${std} - ${msg_playingtime}" + else + msg_albumline="${msg_albumline} - ${msg_playingtime}" + fi + else + msg_albumline="" + fi + fi +} + +function display_column() { + label="$1" + value_file="$2" + value_mpd="$3" + color_mpd="$4" + value_output="$5" + color_output="$6" + printf "%-11s %-18s: %8s ${color_mpd}> %8s ${color_output}> %8s${std}\n" \ + " " \ + "${label}" \ + "${value_file}" \ + "${value_mpd}" \ + "${value_output}" +} + +function display_headerrow() { + input="$1" + throughput="$2" + output="$3" + printf "%-11s %-18s ${white}%8s${std} ${white}%8s${std} ${white}%8s${std}\n" \ + " " \ + " " \ + "${input}" \ + "${throughput}" \ + "${output}" } unset mpc_vals @@ -829,41 +979,24 @@ declare -A soxi_vals ## input | nfs://srv/media/music/.../track11.aiff ## | AIFF (Little Endian) PCM in 24bit/192kHz @ 4.503bit/s -label_input="Storage" +if [[ "${skip_file_check}x" != "x" ]]; then + label_input="" +else + label_input="Storage" +fi label_throughput="MPD" label_output="DAC" label_samplerate="Sample rate (Hz)" label_bitdepth="Bit depth (bit)" label_bitrate="Bit rate (kbit/s)" -main_file_bitdepth= -main_file_bitrate= -main_file_bitrate_kvalue= -main_file_bitrate_raw= -main_file_bitrate_unit= -main_file_bitrate_value= -main_file_samplerate= -main_file_sizebytes= -main_mpd_bitdepth= -main_mpd_bitrate= -main_mpd_samplerate= -main_output_bitdepth= -main_output_samplechannelcount= -main_output_sampleencoding= -main_output_samplerate= -main_output_samplerate_raw= -main_playing_trackalbumartist= -main_playing_trackalbumname= -main_playing_trackalbumnumber= -main_playing_trackartist= -main_playing_trackduration= -main_playing_trackperformers= -main_playing_trackplayingpercentage= -main_playing_trackplayingtime= -main_playing_tracktitle= -main_rt_tracknumber_album= -main_rt_tracknumber_playlist= -main_rt_playlistlength= +input_bitdepth= +input_sizebytes= +mpd_bitdepth= +mpd_bitrate= +mpd_samplerate= +output_bitdepth= +output_samplerate= declare -A alsa_encoding declare -a mpc_fields=( @@ -888,7 +1021,7 @@ declare -a mpc_fields=( ) mpc_format_string= -for mpc_field in ${mpc_fields[@]}; do +for mpc_field in "${mpc_fields[@]}"; do mpc_format_string+="${mpc_field}:[%${mpc_field}%]\n" done