#!/usr/bin/env bash
#-------------------------------------------------------------------------------
# redoflacs - Parallel BASH commandline FLAC compressor, verifier, organizer,
#             analyzer, and retagger
#-------------------------------------------------------------------------------
# ~ THIS IS THE UNIX/LINUX/BSD VERSION OF REDOFLACS ~
#-------------------------------------------------------------------------------
# Copyright (C) 2010-2014  Jaren Stangret
#-------------------------------------------------------------------------------
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#-------------------------------------------------------------------------------
# You can follow development of this script on Github at:
# https://github.com/sirjaren/redoflacs
#
# Please submit requests/changes/patches and/or comments
#-------------------------------------------------------------------------------
# File Descriptors used in this script:
#    0: STDIN
#    1: STDOUT
#    2: STDERR
#    3: Jobs process manager (FIFO)
#    4: auCDtect's STDOUT output
#-------------------------------------------------------------------------------

_info()  { printf " ${green}*${reset} ${@}";  }   # Bold green message
_warn()  { printf " ${yellow}*${reset} ${@}"; }   # Yellow message
_error() { printf " ${red}*${reset} ${@}";    }   # Bold red message

#-------------------------------------------------------------------------------

_long_help()
{
	# Display a lot of help
	#--
	# Set up local variables
	declare columns
	declare -x MANWIDTH

	# Determine terminal width, so we can force 'man' to use the entire terminal
	# width, since reading from /dev/stdin, MANWIDTH will be unset
	#--
	# Redirecting '/dev/stderr' to 'stty' allows valid arguments
	read -r _ _ _ _ _ _ columns _ < <(stty -a < /dev/stderr)
	MANWIDTH="${columns%;}"  # Terminal width - remove trailing semicolon

	# Read in man page from STDIN (not POSIX; /dev/stdin -> /proc/self/fd/0)
	man /dev/stdin << MAN_EOF
.TH "REDOFLACS" 1
.SH NAME
redoflacs \- Parallel BASH commandline FLAC compressor, verifier, organizer, analyzer, and retagger
.SH SYNOPSIS
.B redoflacs
[\fIoperations\fR]
[\fIoptions\fR]
[\fItarget\fR]
.RI ...
.SH DESCRIPTION
.B redoflacs
is a BASH commandline program providing a series of operations to help
manage and verify a user's FLAC music library.  One of the key features of
\fBredoflacs\fP is it's ability to process a great number of FLAC files in parallel,
using as many jobs to complete an operation as possible, very similar to 'GNU
make'.
.P
\fBredoflacs\fP searches for a config file (if run as a user) in:
.P
.nf
.RS
\fB~/.config/redoflacs/config\fP
.RE
.fi
.P
or (if run as root) in:
.P
.nf
.RS
\fB/etc/redoflacs.conf\fP
.RE
.fi
.P
If a config file is not found (in either place), one is created.
.P
More information can be found at <\fIhttps://github.com/sirjaren/redoflacs\fR>.
.SH OPERATIONS
.TP
.B \-c, \-\-compress
.RS
Compress the FLAC files with the user-specified level of compression defined
from the configuration file as '\fBcompression_level\fP' and verify the resultant
files.
.P
The default is 8, with the range of values starting from 1 to 8 with the
smallest compression at 1, and the highest at 8.  This option will add a tag
(\fBVORBIS_COMMENT\fP) to all successfully verified FLAC files.  Below shows the
default \fBCOMPRESSION\fP tag added to each successfully compressed (and verified)
FLAC file:
.P
.nf
.RS
\fBCOMPRESSION=8\fP
.RE
.fi
.P
If any FLAC files already have the defined \fBcompression_level\fP tag (a good
indicator the files are already compressed at that level), the script will
instead test the FLAC files for any errors.  This is useful to check your entire
music library to make sure all the FLAC files are compressed at the level
specified as well as make sure they are intact (ie, not corrupt).
.P
If any files are found to be corrupt, this script will quit upon finishing the
compression of any other files and produce an error log.
.RE
.TP
.B \-C, \-\-compress-notest
.RS
Same as the '\fB\-c, \-\-compress\fP' option, but if any FLAC files already have the
defined compression_level tag, the script will skip the file and continue on to
the next without testing the FLAC file's integrity.  Useful for checking if all your
FLAC files are compressed at the level specified.
.RE
.TP
.B \-t, \-\-test
.RS
Same as '\fB\-c, \-\-compress\fP' but instead of compressing the FLAC files, this script
just verifies their integrity.  This option will NOT add the \fBCOMPRESSION\fP
tag to the files.
.P
As with the '\fB\-c, \-\-compress\fP' option, this will produce an error log if any FLAC
files are found to be corrupt.
.RE
.TP
.B \-a, \-\-aucdtect
.RS
Use the \fBauCDtect\fP program by Oleg Berngardt and Alexander Djourik to analyze
FLAC files and check, with fairly accurate precision, whether the FLAC files are
lossy sourced or not.  For example, an MP3 file converted to FLAC is no longer
lossless therefore lossy sourced.
.P
While this program isn't foolproof, it gives a good idea which FLAC files will
need further investigation (ie, a spectrogram).  This program does not work on
FLAC files which have a bit depth of more than a typical audio CD (16bit), and
will skip the files that have a higher bit depth.
.P
If any files are found to not be perfect (ie, 100% CDDA via auCDtect), a log
will be created with the questionable FLAC files recorded in it.
.RE
.TP
.B \-A, \-\-aucdtect-spectrogram
.RS
Same as '\fB\-a, \-\-aucdtect\fP' with the addition of creating a spectrogram for each
FLAC file that fails \fBauCDtect\fP, that is, any FLAC file that does not return 100%
CDDA from \fBauCDtect\fP will be scanned and a spectrogram will be created.
.P
Any FLAC file skipped (due to having a higher bit depth than 16), will NOT have
a spectrogram created.
.P
By default, each spectrogram will be created in the same folder as the tested
FLAC file name as follows:
.P
.nf
.RS
\fB<filename>__<processed number>__.png\fP
.RE
.fi
.P
An example of this:
.P
.nf
.RS
\fB03 - Some FLAC File.flac\fP      # 7th file processed
\fB03 - Some FLAC File__7__.png\fP  # Spectrogram
.RE
.fi
.P
The user can change the location of where to store the created spectrogram
images by changing the value of '\fBspectrogram_location\fP' defined in the
configuration file.  The location defined by the user will be tested to see if
it exists before starting the script.  If the location does NOT exist, the
script will warn the user and exit.
.P
The created PNG file is large in resolution to best capture the FLAC file's
waveform (\fB~1800x513\fP).
.P
The spectrogram is created using the program \fBSoX\fP.  If the user tries to use this
option without having \fBSoX\fP installed, the script will warn the user that \fBSoX\fP is
missing and exit.
.RE
.TP
.B \-m, \-\-md5check
.RS
Check FLAC files for unset MD5 Signatures, logging any files that have this value unset.
An unset MD5 signature doesn't necessarily mean a FLAC file is corrupt, and MAY be
repaired with a re-encoding of said FLAC file.
.RE
.TP
.B \-e, \-\-extract-artwork
.RS
Run through each FLAC file and extract any and all artwork that's embedded
within the \fBPICTURE\fP block.  This is useful in the event a user wants to save any
artwork before using the '\fB\-p, \-\-prune\fP' option to remove the artwork.
.P
By default, each extracted image will be placed in a subdirectory where the FLAC
file is located.  The subdirectory housing the extracted artwork will have a
similar name as the currently processed FLAC.  If a directory already exists, an
integer is appended to the directory (to prevent overwriting and mixing files).
For example:
.P
.nf
.RS
\fB/path/to/01_file.flac\fP          # FLAC file with embedded artwork
\fB/path/to/01_file.flac_art/\fP     # Directory housing artwork
\fB/path/to/01_file.flac_art~1~/\fP  # Directory '1' if above directory exists
\fB/path/to/01_file.flac_art~2~/\fP  # Directory '2' if above directory exists
\fB/path/to/01_file.flac_art~N~/\fP  # Directory 'N' if above directory exists
.RE
.fi
.P
The user can change the location of where to store the extracted images by
changing the value of '\fBartwork_location\fP' defined in the configuration file.
The location defined by the user will be tested to see if it exists before starting
the script.  If the location does not exist, the script will warn the user and
exit.
.P
This operation supports all the various types of embedded artwork that
\fBmetaflac\fB supports.
.P
If there is more than one image of the same type, this operation will append an
integer after the image filename to prevent clobbering:
.P
.nf
.RS
\fB/path/to/01_file.flac_art~2~/\fP                    # Directory housing art
\fB/path/to/01_file.flac_art~2~/11_Composer.jpg\fP     # Extracted image
\fB/path/to/01_file.flac_art~2~/11_Composer.jpg~1~\fP  # Another image '1'
\fB/path/to/01_file.flac_art~2~/11_Composer.jpg~2~\fP  # Another image '2'
\fB/path/to/01_file.flac_art~2~/11_Composer.jpg~N~\fP  # Another image 'N'
.RE
.fi
.RE
.TP
.B \-p, \-\-prune
.RS
Delete every \fBMETADATA\fP block in each FLAC file except the \fBSTREAMINFO\fP and
\fBVORBIS_COMMENT\fP block.  If '\fBremove_artwork\fP' is set to any value but '\fBtrue\fP'
(via the configuration file) then the \fBPICTURE\fP block will NOT be removed.
.RE
.TP
.B \-g, \-\-replaygain
.RS
Add ReplayGain values to FLAC files.  ReplayGain is calculated for \fBALBUM\fP and
\fBTRACK\fP values and applied via \fBVORBIS_COMMENTS\fP and as such, will require the '\fB\-r, \-\-retag\fP'
option to have these tags kept (see '\fB\-r, \-\-retag\fP' option) in order
to preserve the added ReplayGain values.  The tags added are:
.P
.nf
.RS
\fBREPLAYGAIN_REFERENCE_LOUDNESS\fP
\fBREPLAYGAIN_TRACK_GAIN\fP
\fBREPLAYGAIN_TRACK_PEAK\fP
\fBREPLAYGAIN_ALBUM_GAIN\fP
\fBREPLAYGAIN_ALBUM_PEAK\fP
.RE
.fi
.P
NOTE: This option ignores any ReplayGain tags that may already be set, removing
existing values before applying new ones.
.P
In order for ReplayGain values to be applied correctly, the script has to
determine which FLAC files to add values to by looking at the directory housing
said files.  That is, the script must add ReplayGain values by working off the
FLAC files' parent directory.  If there are some FLAC files found, the script
will move up one directory and begin applying ReplayGain values.  This is
necessary in order to get the \fBREPLAYGAIN_ALBUM_GAIN\fP and \fBREPLAYGAIN_ALBUM_PEAK\fP
values set correctly.  Without doing this, the \fBALBUM\fP and \fBTRACK\fP values would be
identical.
.P
If a user has many FLAC files under one directory (of different albums/artists),
the ReplayGain \fBALBUM\fP values are going to be incorrect as the script will
perceive all those FLAC files to essentially be from the same album.  This is
mitigated by having each album in a separate directory.  Keep in mind,
multi-disc albums must be in separate directories in order to be processed with
different \fBALBUM GAIN\fP and \fBALBUM PEAK\fP values.
.P
If there are any errors found while generating and/or applying ReplayGain
values, an error log will be produced.
.RE
.TP
.B \-G, \-\-replaygain-noforce
.RS
Same as '\fB\-g, \-\-replaygain\fP' but will check for existing ReplayGain tags
BEFORE re-applying new ones.  If any one of the five ReplayGain tags (mentioned
above) are missing from any FLAC file, the script will apply new values to each
FLAC file in that directory (first removing the old ReplayGain tags, if any).
.P
If all five ReplayGain tags are intact in every FLAC file (in a given
directory), that directory will be skipped and no new ReplayGain tags will be
added.
.P
.RE
.TP
.B \-r, \-\-retag
.RS
Extract the configured tags in each FLAC file and clear the rest before
retagging the file.  The default tags kept are:
.P
.nf
.RS
\fBTITLE\fP
\fBARTIST\fP
\fBALBUM\fP
\fBDISCNUMBER\fP
\fBDATE\fP
\fBTRACKNUMBER\fP
\fBTRACKTOTAL\fP
\fBGENRE\fP
\fBCOMPRESSION\fP
\fBRELEASETYPE\fP
\fBSOURCE\fP
\fBMASTERING\fP
\fBREPLAYGAIN_REFERENCE_LOUDNESS\fP
\fBREPLAYGAIN_TRACK_GAIN\fP
\fBREPLAYGAIN_TRACK_PEAK\fP
\fBREPLAYGAIN_ALBUM_GAIN\fP
\fBREPLAYGAIN_ALBUM_PEAK\fP
.RE
.fi
.P
The characters allowed in a tag field are ASCII only (including the SPACE character).
The EQUAL sign (=), is not allowed as this is the delimiter separating tag field and
tag value.
.P
See this link for more details (under 'Content vector format'):
.RS
<\fIhttp://xiph.org/vorbis/doc/v-comment.html\fR>
.RE
.P
If any FLAC files have missing tags (from those configured to be kept), the file
and the missing tag will be recorded in a log.
.P
The tags that can be kept are essentially infinite, as long as the tags to be
kept are set in the \fBTAGGING SECTION\fP of the configuration file.
.P
If this option is specified, a warning will appear upon script execution.  This
warning will show which of the configured \fBTAG\fP fields to keep when re-tagging the
FLAC files.  A countdown will appear giving the user \fB10\fP seconds to abort the
script.
.RE
.TP
.B \-l, \-\-all
.RS
This option is short for:
.P
.nf
.RS
\fB\-c, \-\-compress\fP
\fB\-m, \-\-md5check\fP
\fB\-p, \-\-prune\fP
\fB\-g, \-\-replaygain\fP
\fB\-r, \-\-retag\fP
.RE
.fi
.RE
.TP
.B \-L, \-\-reallyall
.RS
This option is short for:
.P
.nf
.RS
\fB\-c, \-\-compress\fP
\fB\-m, \-\-md5check\fP
\fB\-p, \-\-prune\fP
\fB\-g, \-\-replaygain\fP
\fB\-r, \-\-retag\fP
\fB\-e, \-\-extract-artwork\fP
\fB\-A, \-\-aucdtect-spectrogram\fP
.RE
.fi
.RE
.SH OPTIONS
.TP
.B \-j[\fIN\fR], \-\-jobs[\fI=N\fR]
.RS
Set the number of parallel jobs to run on script invocation.  If this is not
set, this script will attempt to find the number of CPU cores available,
using the number found as the number of parallel jobs to run.
.P
If the script is unable to find the number of CPU cores available, the number of
jobs will be set to \fBtwo\fP (\fI2\fR), by default.
.RE
.TP
.B \-n, \-\-no-color
.RS
Turn off color output.
.RE
.TP
.B \-o, \-\-new-config
.RS
Force the creation of a new configuration file.  This option does \fBNOT\fP
overwrite any existing configuration file.
.RE
.TP
.B \-v, \-\-version
.RS
Display script version and exit.
.RE
.TP
.B \-h, \-\-help
.RS
Shows this help message.
.RE
.SH FILES
.TP
.B ~/.config/redoflacs/config
.RS
User configuration file.
.RE
.TP
.B /etc/redoflacs.conf
.RS
System configuration file.
.RE
.SH BUGS
If you find a bug, please report it at:
.RS
<\fIhttps://github.com/sirjaren/redoflacs/issues/new\fR>
.RE
.SH AUTHOR
\fBJaren Stangret\fP <\fIsirjaren@gmail.com\fR>
.SH THANKS
Thanks to all the people whom have provided feedback and support!
.SH REVISION
\fB5\fP
MAN_EOF
}

#-------------------------------------------------------------------------------

_short_help()
{
	# Display short help
	#--
	printf ' Usage: redoflacs [operations] [options] [target] ...\n'
	printf ' Operations:\n'
	printf '   -c, --compress\n'
	printf '   -C, --compress-notest\n'
	printf '   -t, --test\n'
	printf '   -m, --md5check\n'
	printf '   -a, --aucdtect\n'
	printf '   -A, --aucdtect-spectrogram\n'
	printf '   -e, --extract-artwork\n'
	printf '   -p, --prune\n'
	printf '   -g, --replaygain\n'
	printf '   -G, --replaygain-noforce\n'
	printf '   -r, --retag\n'
	printf '   -l, --all\n'
	printf '   -L, --reallyall\n'
	printf ' Options:\n'
	printf '   -j[N], --jobs[=N]\n'
	printf '   -n, --no-color\n'
	printf '   -o, --new-config\n'
	printf '   -v, --version\n'
	printf '   -h, --help\n'
	printf " This is the short help; for details use 'redoflacs --help' or 'redoflacs -h'\n"
}

#-------------------------------------------------------------------------------

# Display usage
_usage() { printf ' Usage: redoflacs [operations] [options] [target] ...\n'; } >&2

#-------------------------------------------------------------------------------

_message_log_exists()
{
	# Print out the correct operational message regarding a log file's existence
	# and what infomration that log may contain
	#--
	# $1 determines the log file to use, as well as how many lines are printed
	# out to correctly set the current row.  Possible values:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#--
	case "$1" in
		'aucdtect'*)
			_error 'Some FLAC files may be lossy sourced, please check:\n'
		;;
		'md5_check')
			_error 'The MD5 Signature is unset for some FLAC files or there were\n'
			_error 'issues with some of the FLAC files, please check:\n'
		;;
		'compress_'*|'test'|'replaygain_test'|'extract_images'|'prune')
			_error 'There were issues with some of the FLAC files,\n'
			_error 'please check:\n'
		;;
		'replaygain'*'apply')
			_error 'There were issues adding ReplayGain values,\n'
			_error 'please check:\n'
		;;
		'retag_'*)
			_error 'Some FLAC files have missing tags or there were\n'
			_error 'issues with some of the FLAC files, please check:\n'
		;;
	esac

	# Print the bottom half of the message (uniform across all operations)
	_error "${cyan}${log_file}${reset}\n"
	_error 'for details.\n'
} >&2

#-------------------------------------------------------------------------------

_create_log()
{
	# Take log file from the current operation, prepending a header to it as
	# as well as formatting/aligning log lines
	#--
	# $1 determines the log file to create, and is any one of these values:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#--
	# Set up local variables/arrays
	declare    error_msg  filename  line
	declare -i length
	declare -a header     log_array

	# Set log file and the type of header to prepend based on how this function
	# was called, via $1
	case "${1}" in
		'aucdtect'*)
			header=(
				'--------------------------------------------------------------------------------'
				'                            [ Aucdtect Report Log ]'
				''
				'      This log details which FLAC files have errors when running auCDtect'
				'--------------------------------------------------------------------------------'
			) ;;
		'md5_check')
			header=(
				'--------------------------------------------------------------------------------'
				'                            [ Md5 Check Error Log ]'
				''
				' This log details which FLAC files have errors when checking the MD5 signature'
				'--------------------------------------------------------------------------------'
			) ;;
		'compress_'*)
			header=(
				'--------------------------------------------------------------------------------'
				'                        [ Compress & Verify Error Log ]'
				''
				' This log details which FLAC files have errors when compressing and/or verifying'
				'--------------------------------------------------------------------------------'
			) ;;
		'test')
			header=(
				'--------------------------------------------------------------------------------'
				'                               [ Test Error Log ]'
				''
				'          This log details which FLAC files have errors when testing'
				'--------------------------------------------------------------------------------'
			) ;;
		'replaygain_test')
			header=(
				'--------------------------------------------------------------------------------'
				'                         [ ReplayGain Test Error Log ]'
				''
				' This log details which FLAC files have errors when testing for ReplayGain'
				' compatability'
					'--------------------------------------------------------------------------------'
			) ;;
		'replaygain'*'apply')
			header=(
				'--------------------------------------------------------------------------------'
				'                        [ ReplayGain Apply Error Log ]'
				''
				' This log details which directories have FLAC files that have errors when'
				' applying ReplayGain values'
					'--------------------------------------------------------------------------------'
			) ;;
		'retag_'*)
			header=(
				'--------------------------------------------------------------------------------'
				'                              [ Retag Error Log ]'
				''
				'         This log details which FLAC files have errors when retagging'
				'--------------------------------------------------------------------------------'
			) ;;
		'extract_images')
			header=(
				'--------------------------------------------------------------------------------'
				'                         [ Extract Images Error Log ]'
				''
				' This log details which FLAC files have errors when extracting artwork images'
				'--------------------------------------------------------------------------------'
			) ;;
		'prune')
			header=(
				'--------------------------------------------------------------------------------'
				'                              [ Prune Error Log ]'
				''
				' This log details which FLAC files have errors when pruning METADATA blocks'
				'--------------------------------------------------------------------------------'
			) ;;
	esac

	# Create array of current log file
	mapfile -n0 -t log_array < "${log_file}"

	for line in "${log_array[@]}"; do
		# Find the longest filename in the log file, and store it's length
		#--
		# 'path/to/file.flac${unit_separator}error message' -> 'path/to/file.flac'
		filename="${line%%${unit_separator}*}"

		if (( $(wc -L <<< ${filename}) > length )); then
			length=$(wc -L <<< ${filename})  # 'wc -L' is for apparent length
		fi
	done

	# Log header, truncating old log
	printf '%s\n' "${header[@]}" > "${log_file}"

	for line in "${log_array[@]}"; do
		# Left align filenames and line up error messages before appending to
		# log file
		#--
		# 'path/to/file.flac${unit_separator}error message' -> 'path/to/file.flac'
		filename="${line%%${unit_separator}*}"

		# Use 'wc -L' for apparent length, not number of characters
		filename_length=$(wc -L <<< "${filename}")

		# 'path/to/file.flac${unit_separator}error message' -> 'error message'
		error_msg="${line##*${unit_separator}}"

		# Example line: /media/Music/Artist/Album/file.flac  ->  Error Message
		printf "%s%$((length - filename_length))s  ->  %s\n" "${filename}" '' "${error_msg}" >> "${log_file}"
	done
}

#-------------------------------------------------------------------------------

_message()
{
	# Print out current operation message.
	#--
	# $1 determines which title message to print (if any).  Possible values:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#
	# $2 is the number of processed items (iteration)
	#
	# $3 is conditionally called, and appears if the operation was interrupted:
	#   Interrupted operation: $3 == 'interrupt'
	#     Operation completed: $3 is NULL
	#--
	# Set up local variables
	declare message sub_message
	declare -i issues spacing

	# Print title message if $1 is not NULL
	if [[ -n "${1}" ]]; then
		case "${1}" in
			'aucdtect')            message='Validating with auCDtect'                ;;
			'md5_check')           message='Check MD5 Signature'                     ;;
			'compress_'*)          message="Level ${compression_level} Compression"  ;;
			'test')                message='Test FLACs'                              ;;
			'extract_images')      message='Extracting Artwork'                      ;;
			'replaygain_'*)
				if [[ "${1}" == 'replaygain_test' ]]; then
					message='Applying ReplayGain'
					sub_message='Testing'
				else  #  'replaygain'*'apply'
					sub_message='Applying'
				fi
			;;
			'retag_'*)
				if [[ "${1}" == 'retag_analyze' ]]; then
					message='Retagging FLACs'
					sub_message='Analyzing'
				else  #  'retag_apply'
					sub_message='Re-Tagging'
				fi
			;;
			'prune')
				if [[ "${remove_artwork}" != 'true' ]]; then
					message='Prune METADATA Blocks'  # Keep artwork
				else
					message='Prune METADATA Blocks'  # Remove artwork
				fi
			;;
		esac

		# Print title message, if applicable
		if [[ -n "${message}" && -z "$2" ]]; then
			printf "\033[$(_row)H ${green}*${reset} ${message}\n"
		fi

		# Print sub title message, if applicable
		if [[ -n "${sub_message}" && -z "$2" ]]; then
			printf "\033[$(_row);3H${green}>>${reset} ${sub_message}\n"
		fi
	fi

	# Update title/sub title message only if $2 is not NULL
	if [[ -n "${2}" ]]; then
		issues=$(_num_issues)  # Number of issues

		# Specify color according to operation status
		[[ "${3}" == 'interrupt' ]] && color="${cyan}" || color="${green}"

		# Verbage for singular/plural issues
		(( issues == 1 )) && issue_string='issue ' || issue_string='issues'

		if [[ -n "${sub_message}" ]]; then
			# Space between message and number of items proccessed
			spacing=$((44 - ${#sub_message} - ${#2} - ${#issues} - 14))

			# Sub title message
			printf "\033[2A   ${green}>>${reset} %s%${spacing}s${blue}[ ${color}%d ok${blue} | ${red}%d ${issue_string}${blue} ]${reset}\n" \
				"${sub_message}" '' "$2" "${issues}"
		else
			# Space between message and number of items proccessed
			spacing=$((46 - ${#message} - ${#2} - ${#issues} - 14))

			# Title message
			printf "\033[2A  ${green}*${reset} %s%${spacing}s${blue}[ ${color}%d ok${blue} | ${red}%d ${issue_string}${blue} ]${reset}\n" \
				"${message}" '' "$2" "${issues}"
		fi
	fi
}

#-------------------------------------------------------------------------------

_update_operation_status()
{
	# Update $operation_summary[@] with the current operation status
	#--
	# $1 determines which operational index to update; possible values:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#
	# $2 is the operational status, the values of which, can be:
	#   Operation Completed       Operation Interrupted
	#   Operation Did Not Run
	#--
	case "${1}" in
		'aucdtect'*)          operation_summary['Validate with auCDtect']="${2}" ;;
		'md5_check')          operation_summary['Check MD5 Signature']="${2}"    ;;
		'compress_'*)         operation_summary['Compress FLACs']="${2}"         ;;
		'test')               operation_summary['Test FLACs']="${2}"             ;;
		'replaygain_test')    operation_summary['>> Testing']="${2}"             ;;
		'replaygain'*'apply') operation_summary['>> Applying']="${2}"            ;;
		'retag_analyze')      operation_summary['>> Analyzing']="${2}"           ;;
		'retag_apply')        operation_summary['>> Re-Tagging']="${2}"          ;;
		'extract_images')     operation_summary['Extracting Artwork']="${2}"     ;;
		'prune')              operation_summary['Prune METADATA Blocks']="${2}"  ;;
	esac
}

#-------------------------------------------------------------------------------

_print_item()
{
	# Display the current item being that's to be run through an operation
	#--
	# $1 is the filename to print
	# $2 is the filename length
	# $3 is the number completed, ie [12/345]
	# $4 determines whether this is a sub operational item to print
	#--
	# Set up local variables
	declare -i  print_spacing='1'

	case "$4" in
		'')
			#       08 - track.flac                           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 ))
			printf "\033[${placement};9H%s%${print_spacing}s${magenta}%s${reset}" \
				"$1" '' "${3}"
		;;
		'sub')
			#      50% 08 - track.flac                        [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 ))
			printf "\033[${placement};6H${yellow}%s${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				" 50%" "$1" '' "${3}"
		;;
		'half')
			#   50% 08 - track.flac                           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 ))
			printf "\033[${placement};4H${yellow}%s${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				" 50%" "$1" '' "${3}"
		;;
		'decode')
			#       [decoding->WAV] 08 - track.flac           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 - 16 ))
			printf "\033[${placement};9H${cyan}[decoding->WAV]${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				"$1" '' "${3}"
		;;
		'aucdtect_fast')
			#       [auCDtect:fast] 08 - track.flac           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 - 16 ))
			printf "\033[${placement};9H${cyan}[auCDtect:fast]${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				"$1" '' "${3}"
		;;
		'aucdtect_slow')
			#       [auCDtect:slow] 08 - track.flac           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 - 16 ))
			printf "\033[${placement};9H${cyan}[auCDtect:slow]${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				"$1" '' "${3}"
		;;
		'spectrogram')
			#       [spectral->PNG] 08 - track.flac           [12/345]
			(( max_length >= $2 )) && print_spacing=$(( max_length - $2 - 16 ))
			printf "\033[${placement};9H${cyan}[spectral->PNG]${reset} %s%${print_spacing}s${magenta}%s${reset}" \
				"$1" '' "${3}"
		;;
	esac
}

#-------------------------------------------------------------------------------

_print_status()
{
	# Display the result of current item that was operated on
	#--
	# $1 is the file/dir operation result, of which, can be:
	#   ok   fail   issue   skip
	# $2 is the basename of the file/dir
	# $3 is the filename length
	# $4 determines whether item is a sub operation or which action is being done
	#--
	# Set up local variables
	declare -i print_spacing='0' column_placement='4'

	case "$1" in
		'ok')     color="${green}"   result='100%'  ;;
		'fail')   color="${red}"     result='fail'  ;;
		'issue')  color="${yellow}"  result='chck'  ;;
		'skip')   color="${yellow}"  result='skip'  ;;
	esac

	(( max_length >= $3 )) && print_spacing=$(( max_length - $3 ))

	case "$4" in
		'sub')
			column_placement='6'
		;;
		'decode')
			action="${cyan}[decoding->WAV]${reset} "
			(( max_length >= $3 )) && print_spacing=$(( max_length - $3 - 16 ))
		;;
		'aucdtect_fast')
			action="${cyan}[auCDtect:fast]${reset} "
			(( max_length >= $3 )) && print_spacing=$(( max_length - $3 - 16 ))
		;;
		'aucdtect_slow')
			action="${cyan}[auCDtect:slow]${reset} "
			(( max_length >= $3 )) && print_spacing=$(( max_length - $3 - 16 ))
		;;
		'spectrogram')
			action="${cyan}[spectral->PNG]${reset} "
			(( max_length >= $3 )) && print_spacing=$(( max_length - $3 - 16 ))
		;;
	esac

	printf "\033[${placement};${column_placement}H${color}%s${reset} ${action}%s%${print_spacing}s" \
		"${result}" "$2" ''
}

#-------------------------------------------------------------------------------

_print_progress()
{
	# Display filename and progress bar of the current operation
	#--
	# $1 is one of the following operations:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#   decode                    aucdtect_fast       aucdtect_slow
	#   spectrogram
	#
	# $2 is percent
	# $3 is filename to print (may be truncated)
	# $4 is filename length
	#--
	# Set up local variables
	declare  action  progress_bar_length

	case "$1" in
		'decode')
			action="${cyan}[decoding->WAV]${reset} "
			# max_length - 16: '[decoding -> WAV] ' is 18 characters long
			progress_bar_length=$(( ( ( max_length - 16 ) * $2 ) / 100 ))
		;;
		'aucdtect_fast')
			action="${cyan}[auCDtect:fast]${reset} "
			# max_length - 16: '[auCDtect - fast] ' is 18 characters long
			progress_bar_length=$(( ( ( max_length - 16 ) * $2 ) / 100 ))
		;;
		'aucdtect_slow')
			action="${cyan}[auCDtect:slow]${reset} "
			# max_length - 16: '[auCDtect: slow] ' is 16 characters long
			progress_bar_length=$(( ( ( max_length - 16 ) * $2 ) / 100 ))
		;;
		'spectrogram')
			action="${cyan}[spectral->PNG]${reset} "
			# max_length - 16: '[spectral->PNG] ' is 16 characters long
			progress_bar_length=$(( ( ( max_length - 16 ) * $2 ) / 100 ))
		;;
		*)
			progress_bar_length=$(( ( max_length * $2 ) / 100 ))
		;;
	esac

	if (( progress_bar_length < $4 )); then
		# Print out the current item name as well as the progress bar
		#--
		# If $progress_bar_length is less than the current item's name length,
		# print out the item's name with the progress bar a part of the name.
		#
		# Otherwise, print out the item's name, and the progress bar after the
		# item's name
		printf "\033[${placement};4H${yellow}%4s${reset} ${action}${invert}%s${reset}" \
			"${2}%" "${3:0:${progress_bar_length}}"
	else
		printf "\033[${placement};4H${yellow}%4s${reset} ${action}${invert}%s%$(( progress_bar_length - $4 ))s${reset}" \
			"${2}%" "${3}" ''
	fi
}

#-------------------------------------------------------------------------------

_trap_sigint()
{
	# Kill any children process and display the correct interrupt message when
	# a user sends SIGINT during script execution.  Perform any cleanup, and
	# check for the existence of a log file before exiting script.
	#--
	# $1 determines which additional cleanup may need to be performed based off
	# of operation interrupted, as well as whether the trap is invoked during a
	# countdown interruption.  Possible values:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#   countdown
	#--
	# Set up local variables
	declare    finished_items
	declare -a jobs_running=( $(jobs -rp) )  # Store running children processes

	_kill_jobs ${jobs_running[@]}            # Kill running children processes

	for (( i=1; i<=items_processed; i++)); do
		# Clear all the operation lines by moving up each line and clearing it,
		# until we are just below the operation's title message
		#--
		printf "\033[$(( post_row - i))H%${columns}s" ''
	done

	# Update title message, don't update if SIGINT was during a countdown
	if [[ "$1" != 'countdown' ]]; then
		# The number of completely finished (ok) items, minus any items
		# interrupted, minus items with issues
		finished_items="$(( iteration - ${#jobs_running[@]} - $(_num_issues) ))"
		_message "${operation}" "${finished_items}" 'interrupt'
	fi

	# Display extra newline if user invoked SIGINT during a countdown
	[[ "$1" == 'countdown' ]] && printf '\n'

	printf " ${green}*${reset} SIGINT received, generating summary...\n"

	case "${1}" in
		# Remove temporary script-created files
		#--
		'aucdtect'*)   rm -f "${directory}"/**/*_redoflacs_"$$".wav  ;;
		'compress_'*)  rm -f "${directory}"/**/*.tmp,fl-ac+en\'c     ;;
	esac

	# Update status for the current operation, but not if SIGINT was invoked
	# during a countdown
	if [[ "$1" != 'countdown' ]]; then
		_update_operation_status "${1}" 'Operation Interrupted'
	fi

	if [[ -f "${log_file}" ]]; then
		_message_log_exists "${1}"   # Print out log exists to STDERR
		_create_log "${1}"           # Create and format log
	fi

	stty ${old_stty} < /dev/stderr  # Restore old stty settings
	printf '\033[?25h'              # Restore the cursor
	rm -f "${job_fifo}"             # Remove temporary FIFO
	rm -f "${tmp_picture_blocks}"   # Remove temporary 'metaflac' block streams
	_summary                        # Display Summary Of Operations

	exit 130
}

#-------------------------------------------------------------------------------

# Display redoflacs version
_print_version() { printf 'Version %s\n' "${version}"; }

#-------------------------------------------------------------------------------

_metaflac_version()
{
	# Return metaflac version
	#--
	# Metaflacs version (ie: '2' in 1.2.1)
	IFS='.' read -r _ metaflac_version _ < <(metaflac --version)
	printf '%s' "${metaflac_version}"
}

#-------------------------------------------------------------------------------

_kill_jobs()
{
	# Kill any children process (obtained via $@), hiding errors and suppressing
	# the shell's notification of terminated jobs
	#--
	for pid in $@ ; do
		kill ${pid} 2>/dev/null
		wait ${pid} 2>/dev/null
	done
}

#-------------------------------------------------------------------------------

_row()
{
	# Print out the current cursor row position
	#--
	declare old_stty  row_pos        # Intialize local variables
	exec < /dev/tty                  # Set a new TTY to read in STDIN
	old_stty="$(stty -g)"            # Store current TTY settings
	stty raw -echo min 0             # Current TTY set at an absolute minimum
	printf '\033[6n' > /dev/tty      # Send escape into new TTY

	# Read in escape sequence output from TTY.  The escape sequence looks like:
	#   ^[[<integer>;<integer>R
	#--
	# This is what is read in below:  ^[[<integer>
	IFS=';' read -r -d'R' row_pos _ < /dev/tty

	stty "${old_stty}"               # Restore the old TTY settings
	printf '%s' "${row_pos#??}"      # Return row position (removes: ^[)
}

#-------------------------------------------------------------------------------

_scroll_terminal()
{
	# Scroll the terminal, dependant on the number of jobs to process.  If it's
	# not necessary, scrolling may not occur
	#--
	# $1 is the current cursor position in number of rows
	#--
	# Set up local variables
	declare    lines   remaining_lines   to_scroll
	declare -g columns

	# Redirecting '/dev/stderr' to 'stty' allows valid arguments
	read -r _ _ _ _ lines _ columns _ < <(stty -a < /dev/stderr)

	columns="${columns%;}"   # Terminal width - remove trailing semicolon
	lines="${lines%;}"       # Terminal height - remove trailing semicolon

	if (( ${#total_items[@]} < jobs )); then
		# Determine the remaining lines to the bottom of the terminal screen
		#--
		# If there are less items to process than jobs specified, add the
		# difference of lines to the remaining lines (obtained by the total number
		# of lines in the terminal minus the current row position)
		remaining_lines=$(( lines - $1 + (jobs - ${#total_items[@]}) ))
	else
		remaining_lines=$(( lines - $1 ))
	fi

	if (( jobs > remaining_lines )); then
		# Scroll the terminal if there are more jobs than lines available
		#--
		to_scroll=$(( jobs - remaining_lines ))  # Number of lines to scroll

		# Scroll terminal by printing as many newlines as determined above, plus 1
		for ((i=0 ; i<=to_scroll ; i++)); do
			printf '\n'
		done

		printf "\033[$((to_scroll + 1))A" # Place cursor up ($to_scroll + 1) lines
	fi
}

#-------------------------------------------------------------------------------

_get_percent_complete()
{
	# Return an operation's completion percentage, expressed as an integer
	#--
	# $1 is the operation to choose how to obtain the percentage, which can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#   decode
	#
	# $2 is a (possibly multiline) string of text from a command binary's output
	# with a percentage string contained within
	#--
	# Set up local variables
	declare percent

	case "$1" in
		'compress_'*)
			percent="${2//$'\b'/}"                        # Remove backspaces
			percent="${percent##*: }"                     # complete0, ratio=0.307
			percent="${percent##*complete, ratio=?.???}"  # Percent (integer)
		;;
		'test')
			percent="${2##* }"                            # Percent (integer)
		;;
		'decode')
			percent="${2//$'\b'}"                         # Remove backspaces
			percent="${percent##*: }"                     # complete0
			percent="${percent##*complete}"               # Percent (integer)
		;;
		'aucdtect')
			percent="${2##*[}"                            # Percent (integer)
		;;
		'spectrogram')
			percent="${2##*In:}"                          # Percent (floating)
			percent="${percent%%.*}"                      # Percent (integer)
		;;
	esac

	# Default to '0' if percentage is not an integer
	if [[ "${percent}" =~ ^[[:digit:]]+$ ]]; then
		printf '%d' $percent
	else
		printf '%d' '0'
	fi
}

#-------------------------------------------------------------------------------

_new_config()
{
	# Force the creation of a new configuration file
	#--
	# Check if configuration file exists based of ${EUID}.  If it doesn't
	# exist, create one
	if (( EUID == 0 )); then
		# User is root
		#--
		# Configuration file location
		config_file='/etc/redoflacs.conf'

		# If there already is a configuration file, do not overwrite it
		if [[ -f "${config_file}" ]]; then
			config_file="/etc/_$$.redoflacs.conf"
		fi
	else
		# User is _NOT_ root
		#--
		# Configuration file location
		config_file="${HOME}/.config/redoflacs/config"

		# If there already is a configuration file, do not overwrite it
		if [[ -f "${config_file}" ]]; then
			config_file="${HOME}/.config/redoflacs/_$$.config"
		fi
	fi

	# Creates the (new) configuration file
	_create_config

	# Explain to user where to find the new configuration file
	_info 'A new configuration file has been created here:\n'
	_info "${cyan}${config_file}${reset}\n\n"

	_info "It's recommended to review the new configuration file\n"
	_info 'and transfer over any changes you made in your old\n'
	_info 'configuration file.\n\n'

	_info 'After making the changes (if any), rename the new\n'
	_info 'configuration file to your old configuration file\n'
	_info 'name.  Here is the command you could use:\n'

	if (( EUID == 0 )); then
		_info "${cyan}mv${reset} ${cyan}${config_file}${reset} ${cyan}/etc/redoflacs.conf${reset}\n"
	else
		_info "${cyan}mv${reset} ${cyan}${config_file}${reset} ${cyan}${HOME}/.config/redoflacs/config${reset}\n"
	fi
}

#-------------------------------------------------------------------------------

_create_config()
{
	# Create a configuration file
	#--
	# Set up local array
	declare -a config

	# Don't expand variables when using heredoc
	mapfile -n0 -t config <<- "END_OF_CONFIG"
		################################################################################
		#                                                                              #
		#                         REDOFLACS USER CONFIGURATION                         #
		#                         ----------------------------                         #
		#                                                                              #
		#  Any line that is _NOT_ prepended with a '#' will be interpreted as an       #
		#  option (except for blank lines -- these are not interpreted)                #
		#                                                                              #
		#  See 'redoflacs --help' for a detailed description of each parameter         #
		#                                                                              #
		################################################################################

		#-------------------------------------------------------------------------------
		# TAGGING SECTION
		#-------------------------------------------------------------------------------
		# List the tags to be kept in each FLAC file.  The default is listed below.
		#
		# Another common tag not added by default is ALBUMARTIST.  Uncomment ALBUMARTIST
		# below to allow script to keep this tag.
		#
		# NOTE: Whitespace _IS_ allowed for the these tag fields, ie:
		#   ALBUM ARTIST
		#   CATALOG NUMBER ISBN
		TITLE
		ARTIST
		#ALBUMARTIST
		ALBUM
		DISCNUMBER
		DATE
		TRACKNUMBER
		TRACKTOTAL
		GENRE

		# The COMPRESSION tag is a custom tag to allow the script to determine which
		# level of compression the FLAC file(s) has/have been compressed at.
		COMPRESSION

		# The RELEASETYPE tag is a custom tag the author of this script uses to
		# catalogue what kind of release the album is (ie, Full Length, EP, Demo, etc.).
		RELEASETYPE

		# The SOURCE tag is a custom tag the author of this script uses to catalogue
		# which source the album has derived from (ie, CD, Vinyl, Digital, etc.).
		SOURCE

		# The MASTERING tag is a custom tag the author of this script uses to catalogue
		# how the album has been mastered (ie, Lossless, or Lossy).
		MASTERING

		# The REPLAYGAIN tags below, are added by the '-g, --replaygain' or
		# '-G, --replaygain-noforce' argument.  If you want to keep the ReplayGain tags,
		# make sure you leave these here.
		REPLAYGAIN_REFERENCE_LOUDNESS
		REPLAYGAIN_TRACK_GAIN
		REPLAYGAIN_TRACK_PEAK
		REPLAYGAIN_ALBUM_GAIN
		REPLAYGAIN_ALBUM_PEAK

		#-------------------------------------------------------------------------------
		# OPTIONS
		#-------------------------------------------------------------------------------
		# REMOVE ARTWORK
		#--
		# Set whether to remove embedded artwork within FLAC files.  By default, this
		# script will remove any artwork it can find in the PICTURE block of a FLAC
		# file.  Set 'remove_artwork' as 'true' to remove embedded artwork.  All other
		# values are intepreted as 'false'.
		remove_artwork="true"

		# SET COMPRESSION
		#--
		# Set the type of COMPRESSION strength when compressing the FLAC files.  Numbers
		# range from '1-8', with '1' being the lowest compression and '8' being the
		# highest compression.  The default is '8'.
		compression_level="8"

		# ERROR LOG DIRECTORY
		#--
		# Set where you want error logs to be placed.  By default, they are stored in
		# the user's HOME directory.
		error_log="${HOME}"

		# AUCDTECT SKIP LOSSY
		#--
		# Set whether FLAC files should be skipped if the MASTERING tag is already set
		# as 'Lossy' when analyzed with auCDtect.  Set 'skip_lossy' as 'true' to to skip
		# FLAC files that have the tag: 'MASTERING=Lossy'.  All other values are
		# intepreted as 'false'.
		skip_lossy="true"

		# SPECTROGRAM DIRECTORY
		#--
		# Set where created spectrogram images should be stored.  By default, they are
		# stored in the same directory as the analyzed FLAC files.  Each image will have
		# the same name as the tested FLAC file but with an integer suffix indicating
		# the FLAC number (which was processed by the script) to allow for uniqueness.
		# The type of image created is PNG with the extension '.png'.
		#
		# All values for 'spectrogram_location' are interpreted as a directory.  If left
		# blank, the default location will be used.
		#
		# An example of a user-defined location:
		#    spectrogram_location="${HOME}/Spectrogram_Images"
		#
		# See '--help' or '-h' for more information.
		spectrogram_location=""

		# EXTRACTED ARTWORK DIRECTORY
		#--
		# Set where the extracted artwork images should be stored.
		#
		# By  default, each extracted image will be placed in a subdirectory where the
		# FLAC file is located.  The subdirectory housing the extracted artwork will
		# have a similar name as the currently processed FLAC.  If a directory already
		# exists, an integer is appended to the directory (to prevent overwriting and
		# mixing files).  For example:
		#
		#    /path/to/01_file.flac          # FLAC file with embedded artwork
		#    /path/to/01_file.flac_art/     # Directory housing artwork
		#    /path/to/01_file.flac_art~1~/  # Directory '1' if above directory exists
		#    /path/to/01_file.flac_art~2~/  # Directory '2' if above directory exists
		#    /path/to/01_file.flac_art~N~/  # Directory 'N' if above directory exists
		#
		# All values for 'artwork_location' are interpreted as a directory.  If left
		# blank, the default location will be used.
		#
		# If there is a user-defined location, the extracted images will be placed in a
		# subdirectory in that location with a naming scheme similar to above:
		#
		#    artwork_location="${HOME}/artwork"    # User-defined configuration option
		#
		#    /path/to/01_file.flac                 # FLAC file with embedded artwork
		#    ${HOME}/artwork/01_file.flac_art/     # Directory housing artwork
		#    ${HOME}/artwork/01_file.flac_art~1~/  # Directory '1' if above directory exists
		#
		# See '--help' or '-h' for more information.
		artwork_location=""

		# PREPEND TRACK NUMBER
		#--
		# Change whether the '-r, --retag' operation will re-tag singular track numbers
		# and track totals from:
		#    1, 2, 3, 4, 5, 6, 7, 8, 9
		# to
		#    01, 02, 03, 04, 05, 06, 07, 08, 09
		#
		# For example, if you had:
		#    TRACKNUMBER=4
		#     TRACKTOTAL=9
		#
		# You would end up with:
		#    TRACKNUMBER=04
		#     TRACKTOTAL=09
		#
		# This is enabled by setting 'prepend_zero' option as 'true'.  All other values
		# are interpreted as 'false'.
		prepend_zero="false"

		## CONFIG REVISION 3 :: DO NOT DELETE THIS LINE ##
	END_OF_CONFIG

	# Write out configuration to either system-wide or local location
	printf '%s\n' "${config[@]}" > "${config_file}"
}

#-------------------------------------------------------------------------------

_parse_config()
{
	# Parse the user/system configuration file
	#--
	# Load the config file into an array and process each line, grabbing the
	# user-specified FLAC tags and setting up the configuration variables
	#--
	# Set up local variables
	declare  config_option

	while read -r line; do
		# Run through the config file, evaluating the configuration option into the
		# current enviroment and storing the tag fields into an array
		#--
		# Test and use only the tag and config options
		if [[ -n "${line###*}" && -n "${line}" ]]; then
			config_option="${line//*=*/}"  # Null if line is a config option

			if [[ -n "${config_option}" ]]; then
				tags+=( "${line^^}" )       # Store uppercase tag in array
			else
				eval "${line}"              # Put config option in environment
			fi
		fi
	done < "${config_file}"
}

#-------------------------------------------------------------------------------

_check_config_version()
{
	# Check current configuration, if the version in the script is newer
	# warn user and display a countdown before starting script
	#--
	# Set up local variables/arrays
	declare    config_file  config_last_line  revision
	declare -a config_array

	# Check if configuration file exists based of ${EUID}.  If it doesn't
	# exist, create one
	if (( EUID == 0 )); then
		# User is root
		#--
		# Configuration file location
		config_file='/etc/redoflacs.conf'
	else
		# User is _NOT_ root
		#--
		# Configuration file location
		config_file="${HOME}/.config/redoflacs/config"
	fi

	# Load configuration file into an array
	mapfile -n0 -t config_array < "${config_file}"

	# Obtain only the last line of the config
	config_last_line="${config_array[@]: -1}"

	# Extract the revision number from the configuration file (new syntax format as
	# of redoflacs 0.30):
	read -r _ _ _ revision _ <<< "${config_last_line}"

	# Check if ${revision} is an integer.  If not, display countdown and warn user
	# of new configuration file, else test if the user config revision is less than
	# the script config revision
	if [[ "${revision}" =~ ^[[:digit:]]+$ ]]; then
		if (( script_revision > revision )); then
			_countdown_config; printf '\n\n'
		fi
	else
		_countdown_config; printf '\n\n'
	fi
}

#-------------------------------------------------------------------------------

_truncate_filename()
{
	# Truncate the processed item's filename if it's bigger than terminal width,
	# returning the filename (possibly truncated) as well as the length of the
	# filename (in characters, possibly truncated)
	#--
	# $1 is the filename to truncate/process
	# $2 determines whether this is a multi-stage operation (eg, auCDtect)
	#--
	# Set up local variables
	declare    filename="$1"  length_diff
	declare -i filename_length  apparent_length  length

	# Make absolute pathname
	[[ "${filename}" == '.' || "${filename}" == './' ]] && filename="${PWD}"

	# Basename of file/directory
	if [[ "${filename: -1}" == '/' ]]; then         # Last character is a '/'
		filename="${filename%?}"                     # Remove last character
		filename="${filename##*/}/"                  # Basename of directory, append '/'
	else
		filename="${filename##*/}"                   # Basename of file
	fi

	filename="${filename//$'\n'/?}"                 # Replace '\n' with '?'

	# Apparent (column-wdith) length of filename; 'wc' handles wide characters
	filename_length="$(wc -L <<< "${filename}")"

	# Subtract 16 blocks from $max_length if we're doing a multi-stage operation
	[[ "${2}" == 'multi-stage' ]] && max_length=$((max_length - 16))

	# If filename is longer than the width allowed in the terminal, truncate it
	if (( filename_length > max_length )); then
		max_length=$((max_length - 4 ))       # Leave room for space and ellipsis
		apparent_length="${filename_length}"  # Apparent length of item
		length="${max_length}"                # Length in ${foo:offset:length}

		# Use fast ${foo:offset:length} truncate item if the apparent length
		# equals the number of characters using ${#foo}.  Otherwise, use 'wc' to
		# grab apparent length (slower).  The slow method is only used for wide
		# characters
		#--
		if (( ${#filename} == filename_length )); then
			# eg, 'longfilename' -> 'longfilen...'
			filename="${filename:0:${max_length}}..."
			filename_length="${#filename}"
		else
			until (( apparent_length <= max_length )); do
				# Keep slicing off a character from the current item, until it's length
				# is less than or equal to the maximum length allowed.  Wide characters
				# may never truncate equal to $max_length, but may be less than it
				#--
				((--length))
				apparent_length="$(wc -L <<< "${filename:0:${length}}")"
			done

			# Difference between lengths, represented as spaces
			printf -v length_diff "%$(( max_length - apparent_length ))s" ''

			# eg, 'longfilename' -> 'longfilen ...'
			filename="${filename:0:${length}}${length_diff}..."

			# Apparent length of truncated filename
			filename_length="$(wc -L <<< "${filename}")"
		fi
	fi


	# Return (truncated) filename and (truncated) filename length
	printf "%s${unit_separator}%d" "${filename}" "${filename_length}"
}

#-------------------------------------------------------------------------------

_find_cores()
{
	# Determine number of jobs to run via the number CPUs/cores available
	#--
	# Set up global variable
	declare -gi jobs='2'                  # By default, set $jobs to '2'
	declare -g  jobs_display='(Default)'  # Default $jobs determination

	# Check /proc/cpuinfo if /proc is mounted by comparing device numbers to /
	if (( $(stat -c %d '/proc') != $(stat -c %d '/') )); then
		if [[ -f '/proc/cpuinfo' ]]; then
			# /proc/cpuinfo exists, find total number of cores to use
			#--
			# Store contents of /proc/cpuinfo into core_array
			mapfile -n0 -t cores_array < /proc/cpuinfo

			for i in "${cores_array[@]}"; do
				# For each line, add processor number to jobs if matched
				#--
				[[ "${i}" == 'processor'*:' '* ]] && jobs="${i#processor*: }"
			done

			((jobs++))  # +1 to $jobs since 'processor' starts at '0'
			jobs_display='(/proc/cpuinfo)'  # $jobs dynamically determined
		fi
	fi
}

#-------------------------------------------------------------------------------

_find_artwork()
{
	# Find all the artwork blocks in a given FLAC file, storing each instance
	# into an array, to be returned as $artwork[@]
	#--
	# Set up local variables/array
	declare -a artwork_blocks
	declare tmp_picture_blocks="/tmp/redoflacs_block_stream_${BASHPID}"

	# Grab all the PICTURE blocks from current FLAC file, storing into an array
	#--
	# It's much faster to read in from a temporary file than via process
	# substitution
	metaflac --list --block-type=PICTURE "${1}" > "${tmp_picture_blocks}"

	# Continue if there were any PICTURE blocks found in current FLAC
	if [[ -s "${tmp_picture_blocks}" ]]; then
		# Only read in the lines we care about and store into array
		mapfile -n0 -t artwork_blocks < \
		<(
			while read -r; do
				# We only care about the block, picture and MIME type lines
				[[ "${REPLY}" == 'METADATA'* || "${REPLY}" == '  '[tM]* ]] && printf '%s\n' "${REPLY}"
			done < "${tmp_picture_blocks}"
		)

		# Run through each line obtained and parse out the information wanted
		#--
		# $artwork_blocks[@] looks something like this:
		#   'METADATA block #2'
		#   '  type: 6 (PICTURE)'
		#   '  type: 5 (Leaflet page)'
		#   '  MIME type: image/jpeg'
		#   'METADATA block #3'
		#   '  type: 6 (PICTURE)'
		#   '  type: 6 (Media (e.g. label side of CD))'
		#   '  MIME type: image/jpeg'
		#   'METADATA block #4'
		#   '  type: 6 (PICTURE)'
		#   '  type: 7 (Lead artist/lead performer/soloist)'
		#   '  MIME type: image/jpg'
		#--
		for i in "${!artwork_blocks[@]}"; do
			if [[ "${artwork_blocks[$i]}" == 'METADATA'* ]]; then
				block_id="${artwork_blocks[$i]##* #}"  # METADATA block #4 -> 4

				# type: 8 (Artist/Performer) -> art_id='8', art_desc='(Artist-Performer)'
				read -r _ art_id art_desc <<< "${artwork_blocks[$((i + 2))]//\//-}"

				# MIME type: image/jpeg -> 'jpg'
				IFS='/' read -r _ art_ext <<< "${artwork_blocks[$((i + 3))]/jpeg/jpg}"

				# Store artwork information into array as a single index
				artwork+=( "${block_id}:${art_id} ${art_desc}.${art_ext}" )
			fi
		done
	fi

	rm -f "${tmp_picture_blocks}"  # Remove temporary 'metaflac' block streams
}

#-------------------------------------------------------------------------------

_top_banner()
{
	# Top banner displaying invocation settings
	#--
	read -r _ flac_version < <(flac --version)                # Flac Version

	printf " ${blue}%s${reset}\n" \
		'---------------------------------------------------'  # Top title line

	printf '%16sRuntime Information\n'                        # Title

	printf " ${blue}%s${reset}\n" \
		'-------------------------+-------------------------'  # Bottom title line

	printf "                redoflacs ${blue}|${reset} ${cyan}%s${reset}\n" \
		"${version}"                                           # Script version

	printf "                     FLAC ${blue}|${reset} ${cyan}%s${reset}\n" \
		"${flac_version}"                                      # Flac version

	printf "                     Jobs ${blue}|${reset} ${cyan}%s %s${reset}\n" \
		"${jobs}" "${jobs_display}"                            # Number of jobs

	printf "            Log Directory ${blue}|${reset} ${cyan}%s${reset}\n" \
		"${error_log}/"                                        # Log directory

	# Set configuration directory
	if (( EUID == 0 )) ; then
		config_directory='/etc/'                               # System config
	else
		config_directory='~/.config/redoflacs/'                # User config
	fi

	printf "         Config Directory ${blue}|${reset} ${cyan}%s${reset}\n" \
		"${config_directory}"                                  # Config directory

	printf " ${blue}%s${reset}\n" \
		'-------------------------+-------------------------'  # End banner line

	_info 'Finding FLAC files to process...'                  # Show FLAC search
}

#-------------------------------------------------------------------------------

_countdown_metadata()
{
	# Display countdown before retagging to allow user to quit script safely
	#--
	trap '_trap_sigint countdown' SIGINT  # Trap SIGINT to abort cleanly

	# Warning message
	_error "${yellow}CAUTION!${reset} These are the tag fields that will be kept\n" >&2
	_error 'when re-tagging the selected files:\n' >&2

	# Creates the listing of tags to be kept
	printf '     %s\n' "${tags[@]}" >&2

	# Warning message about embedded coverart
	_error "By default, this script will ${cyan}REMOVE${reset} the legacy ${cyan}COVERART${reset} tag.\n" >&2
	_error "Add the ${cyan}COVERART${reset} tag to the list of tags to be kept in the\n" >&2
	_error "${cyan}TAGGING SECTION${reset} of the configuration file.\n\n" >&2

	_error "Keep in mind, if the ${cyan}remove_artwork${reset} option is set to ${cyan}false${reset},\n" >&2
	_error "embedded artwork in the ${cyan}PICTURE${reset} block will be kept when using\n" >&2
	_error "the ${cyan}-p, --prune${reset} option as well.\n\n" >&2

	_warn "Waiting ${red}10${reset} seconds before starting program...\n" >&2
	_warn 'Ctrl+C (Control-C) to abort...\n' >&2
	_info 'Starting in: '

	# 10 second countdown
	for count in {10..1}; do
		printf "${red}%d ${reset}" "$count"
		read -t1  # Sleep 1
	done

	printf '\n'  # Advance countdown to next line
}

#-------------------------------------------------------------------------------

_countdown_config()
{
	# Displays countdown if a newer config is found to allow user to quit safely
	#--
	trap '_trap_sigint countdown' SIGINT  # Trap SIGINT to abort cleanly

	# Warning message
	_info 'There is a newer configuration file available!\n\n'

	_warn 'It is recommended you generate a new configuration\n' >&2
	_warn 'file for use with this program.\n\n' >&2

	_warn 'To generate a new configuration file, run:\n' >&2
	_warn "${cyan}redoflacs --new-config${reset}\n\n" >&2

	_warn 'The above command will _NOT_ overwrite your\n' >&2
	_warn 'current configuration file.\n\n' >&2

	_warn 'Waiting 10 seconds before starting program...\n' >&2
	_warn 'Ctrl+C (Control-C) to abort...\n' >&2
	_info 'Starting in: '

	# 10 second countdown
	for count in {10..1}; do
		printf "${red}%s ${reset}" "$count"
		read -t1  # Sleep 1
	done
}

#-------------------------------------------------------------------------------

_get_directory_list()
{
	# Return a listing of the total base directories housing all the found FLACs
	#--
	declare     previous_dir  current_dir  # Set up local variable(s)
	declare -ga total_dirs                 # Set up global array

	for flac in "$@"; do
		# Run through total FLAC files array, printing out each unique directory
		#--
		current_dir="${flac%/*}"
		if [[ "${previous_dir}" != "${current_dir}" ]]; then
			total_dirs+=( "${current_dir}/" )
		fi
		previous_dir="${current_dir}"       # Set current directory to previous
	done
}

#-------------------------------------------------------------------------------

_clear_jobs_fd()
{
	# Clear job manager file descriptor (tied to FIFO) by closing and reopening
	#--
	# $1 is the FIFO to tie the file descriptor to
	#--
	exec 3<&- 3>&-  # Close file descriptor
	rm -f "$1"      # Remove FIFO if it exists
	mkfifo "$1"     # Create FIFO
	exec 3<>"$1"    # Open file descriptor read/write
}

#-------------------------------------------------------------------------------

_num_issues()
{
	# Return an integer detailing the number of issues an operation may have had,
	# returning '0', if no issues were found
	#--
	declare ticks  # Set up local variable

	# Read in number of issue ticks, hiding missing file output
	{ read -r ticks < "${issue_ticks}"; } 2>/dev/null
	printf '%d' "${#ticks}"  # Return number of ticks (0, if empty)
}

#-------------------------------------------------------------------------------

_process_positional_parameters()
{
	# Obtain and process the positional parameters invoked with the script
	#--
	# Set up global variables
	declare -g all  reallyall  create_spectrogram  no_color  directory

	# Set up local variables/arrays
	declare -a args  long_args  short_args  non_args  converted_args
	declare    regex

	# If no arguments are made to the script show usage & short help
	if (( ${#} == 0 )); then
		_short_help
		exit 1
	fi

	# If only one argument was called
	if (( ${#} == 1 )); then
		case "$1" in
			'--help'|'-h')        _long_help     ; exit 0 ;;
			'--version'|'-v')     _print_version ; exit 0 ;;
			'--new-config'|'-o')  _new_config    ; exit 0 ;;
			*)                    _usage         ; exit 1 ;;
		esac
	fi

	# If only two arguments were called
	if (( ${#} == 2 )); then
		case "$1" in
			# The number of jobs cannot be specified without an operation
			'--jobs='[[:digit:]]*' '|'-j'[[:digit:]]*' ')
				_usage
				_error "${cyan}${1}${reset} cannot used without an operation specified.\n" >&2
				exit 1
			;;
		esac
	fi

	for i in "${@}"; do
		# Separate long, short, and non arguments into separate arrays to be
		# converted into short arguments for 'getopts' to process correctly
		#--
		case "$i" in
			'--'*) long_args+=( "${i}" )   ;;
			'-'*)  short_args+=( "${i}" )  ;;
			*)     non_args+=( "${i}" )    ;;
		esac
	done

	# If there isn't a single non-argument (directory), exit
	if (( ${#non_args[@]} != 1 )); then
		_usage
		exit 1
	fi

	# Long arguments
	#--
	# If any were called, convert long arguments to short, allowing 'getopts' to
	# process them
	#--
	if [[ -n "${long_args[@]}" ]]; then
		for i in "${long_args[@]}"; do
			case "$i" in
				# These arguments are to be called by themselves, so quit
				'--version')              _usage; exit 1          ;;
				'--help')                 _usage; exit 1          ;;
				'--new-config')           _usage; exit 1          ;;

				# Send long arguments to array to process later
				'--aucdtect')             converted_args+=( -a )  ;;
				'--aucdtect-spectrogram') converted_args+=( -A )  ;;
				'--md5check')             converted_args+=( -m )  ;;
				'--compress')             converted_args+=( -c )  ;;
				'--compress-notest')      converted_args+=( -C )  ;;
				'--test')                 converted_args+=( -t )  ;;
				'--replaygain')           converted_args+=( -g )  ;;
				'--replaygain-noforce')   converted_args+=( -G )  ;;
				'--retag')                converted_args+=( -r )  ;;
				'--extract-artwork')      converted_args+=( -e )  ;;
				'--prune')                converted_args+=( -p )  ;;
				'--all')                  converted_args+=( -l )  ;;
				'--reallyall')            converted_args+=( -L )  ;;
				'--no-color')             converted_args+=( -n )  ;;

				'--jobs='*)
					# Enforce we have only digits after '--jobs=', and if so, set the
					# number of $jobs to its value, otherwise exit with a warning
					#--
					regex="[[:digit:]]+$"  # Regular expression
					if [[ "${i##*=}" =~ $regex ]] && (( ${i##*=} != 0 )); then
						jobs="${i##*=}"  # --jobs=11 -> 11
					else
						_usage
						_error "${cyan}--jobs${reset} requires a non-zero integer after it (eg. ${cyan}--jobs=11${reset}).\n" >&2
						exit 1
					fi
				;;

				# All other arguments are invalid
				*)  invalid_args+=( "${i}" )  ;;
			esac
		done
	fi

	# Short arguments
	#--
	# If any were called, add valid short arguments (using 'getopts') to the same
	# array as long arguments
	#--
	if [[ -n "${short_args[@]}" ]]; then
		while getopts ":j:LlcCtgGaAmeprnhvo" args "${short_args[@]}"; do
			case "${args}" in
				# These arguments are to be called by themselves, so quit
				'v') _usage; exit 1          ;;
				'h') _usage; exit 1          ;;
				'o') _usage; exit 1          ;;

				# Send short arguments to array to process later
				'L') converted_args+=( -L )  ;;
				'a') converted_args+=( -a )  ;;
				'A') converted_args+=( -A )  ;;
				'm') converted_args+=( -m )  ;;
				'c') converted_args+=( -c )  ;;
				'C') converted_args+=( -C )  ;;
				't') converted_args+=( -t )  ;;
				'g') converted_args+=( -g )  ;;
				'G') converted_args+=( -G )  ;;
				'r') converted_args+=( -r )  ;;
				'e') converted_args+=( -e )  ;;
				'p') converted_args+=( -p )  ;;
				'l') converted_args+=( -l )  ;;
				'n') converted_args+=( -n )  ;;

				'j')
					# Enforce we have only digits after '-j', and if so, set the
					# number of $jobs to its value, otherwise exit with a warning
					#--
					regex="[[:digit:]]+$"  # Regular expression
					if [[ "${OPTARG}" =~ $regex ]] && (( OPTARG != 0 )); then
						jobs="${OPTARG}"  # OPTARG is the argument after 'j' (j:)
					else
						_usage
						_error "${cyan}-j${reset} requires a non-zero integer after it (eg. ${cyan}-j11${reset}).\n" >&2
						exit 1
					fi
				;;
				?)
					# Set invalid argument from getopts into array using
					# ${OPTARG}
					invalid_args+=( "-${OPTARG}" )
				;;
			esac
		done
	fi

	# Display invalid arguments, if any
	if [[ -n "${invalid_args[@]}" ]]; then
		_usage
		_error 'Invalid option(s): ' >&2
		printf "${cyan}%s${reset}\n" "${invalid_args[*]}" >&2
		exit 1
	fi

	# Process converted arguments, setting each operation to run into an array
	#--
	# Run through the catchall arugments first
	for i in "${converted_args[@]}"; do
		case "${i}" in
			# These are the meta-arguments (do multiple operations)
			'-l')
				all='true'
				operations[1]='md5_check'
				operations[2]='compress_verify'
				operations[5]='replaygain_test'
				operations[6]='replaygain_force_apply'
				operations[7]='retag_analyze'
				operations[8]='retag_apply'
				operations[10]='prune'
			;;
			'-L')
				reallyall='true'
				create_spectrogram='true'
				operations[0]='aucdtect'
				operations[1]='md5_check'
				operations[2]='compress_verify'
				operations[5]='replaygain_test'
				operations[6]='replaygain_force_apply'
				operations[7]='retag_analyze'
				operations[8]='retag_apply'
				operations[9]='extract_images'
				operations[10]='prune'
			;;
		esac
	done

	# Run through the individual operations
	for i in "${converted_args[@]}"; do
		case "${i}" in
			# Process individual arguments
			'-a')
				if [[ "${create_spectrogram}" == 'true' || "${operations[0]}" == 'conflict' ]]; then
					operations[0]='conflict'  # If already set
				else
					operations[0]='aucdtect'
				fi
			;;
			'-A')
				if [[ -n "${operations[0]}" && -z "${create_spectrogram}" ]] || [[ "${operations[0]}" == 'conflict' ]]; then
					operations[0]='conflict'  # If already set
				else
					create_spectrogram='true'
					operations[0]='aucdtect'
				fi
			;;
			'-m') operations[1]='md5_check'         ;;
			'-c') operations[2]='compress_verify'   ;;
			'-C') operations[3]='compress_no_test'  ;;
			'-t') operations[4]='test'              ;;
			'-g')
				if [[ "${operations[6]}" == 'replaygain_apply' || "${operations[6]}" == 'conflict' ]]; then
					operations[6]='conflict'   # If already set
				else
					operations[5]='replaygain_test'
					operations[6]='replaygain_force_apply'
				fi
			;;
			'-G')
				if [[ "${operations[6]}" == 'replaygain_force_apply' || "${operations[6]}" == 'conflict' ]]; then
					operations[6]='conflict'   # If already set
				else
					operations[5]='replaygain_test'
					operations[6]='replaygain_apply'
				fi
			;;
			'-r')
				operations[7]='retag_analyze'
				operations[8]='retag_apply'
			;;
			'-e')
				operations[9]='extract_images'
			;;
			'-p') operations[10]='prune'            ;;
			'-n') no_color='true'                   ;;
		esac
	done

	args=( "${@}" )  # Store arguments into an array to process

	# Obtain the last element in $args[@], which is the directory to process
	#--
	# BASH 4.2 allows negative indices:
	#   directory="${args[-1]%/}"
	directory="${args[$(( ${#args[@]} - 1 ))]%/}"  # Remove ending slash (if any)
}

#-------------------------------------------------------------------------------

_check_missing_programs()
{
	# Check for missing programs vital to this script
	#--
	# Set up local variables/arrays
	declare -a missing_commands

	# Add each command that's needed to an array to be displayed
	if ! type -P rm >/dev/null; then
		missing_commands+=( "   Missing ${cyan}rm${reset}        ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P stty >/dev/null; then
		missing_commands+=( "   Missing ${cyan}stty${reset}      ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P stat >/dev/null; then
		missing_commands+=( "   Missing ${cyan}stat${reset}      ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P mkdir >/dev/null; then
		missing_commands+=( "   Missing ${cyan}mkdir${reset}     ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P mkfifo >/dev/null; then
		missing_commands+=( "   Missing ${cyan}mkfifo${reset}    ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P wc >/dev/null; then
		missing_commands+=( "   Missing ${cyan}wc${reset}        ->  Part of ${cyan}coreutils${reset}" )
	fi
	if ! type -P metaflac >/dev/null; then
		missing_commands+=( "   Missing ${cyan}metaflac${reset}  ->  Part of ${cyan}flac${reset}" )
	fi
	if ! type -P flac >/dev/null; then
		missing_commands+=( "   Missing ${cyan}flac${reset}      ->  Part of ${cyan}flac${reset}" )
	fi

	if [[ -n "${missing_commands[@]}" ]]; then
		# Display message that system is missing vital programs
		#--
		_error 'You seem to be missing one or more necessary programs\n'    >&2
		_error 'to run this script reliably.  Below shows the program(s)\n' >&2
		_error 'missing, as well as where you can install them from:\n'     >&2
		for i in "${missing_commands[@]}"; do
			_warn "${i}\n" >&2
		done
		exit 1
	fi

	# Optional binaries
	#--
	# Check for auCDtect if operation was called
	if [[ "${operations[0]}" == 'aucdtect' ]]; then
		if aucdtect="$(type -P auCDtect)"; then
			_aucdtect_cmd() { "${aucdtect}" "$@"; }  # Normal typeface
		elif aucdtect="$(type -P aucdtect)"; then
			_aucdtect_cmd() { "${aucdtect}" "$@"; }  # Alternate spelling
		else
			# auCDtect/aucdtect cannot be found
			_error "It appears ${cyan}auCDtect${reset} is not installed. Please verify you\n" >&2
			_error "have this program installed and can be found in ${cyan}\$PATH${reset}\n" >&2
			exit 1
		fi

		# Make sure auCDtect is executable
		if [[ ! -x "${aucdtect}" ]]; then
			_error "It appears ${cyan}auCDtect${reset} is not executable.  In order to make\n" >&2
			_error "${cyan}auCDtect${reset} executable, run:\n" >&2
			_error "${cyan}chmod u+x '${aucdtect}'${reset}\n" >&2
			exit 1
		fi
	fi

	# Check for SoX if auCDtect spectrograms were called
	if [[ "${create_spectrogram}" == 'true' ]]; then
		if sox="$(type -P sox)"; then
			_sox_cmd() { "${sox}" "$@"; }
		else
			# SoX cannot be found
			_error "It appears ${cyan}SoX${reset} is not installed. Please verify you\n" >&2
			_error "have this program installed and can be found in ${cyan}\$PATH${reset}\n" >&2
			exit 1
		fi
	fi
}

#-------------------------------------------------------------------------------

_check_conflicting_operations()
{
	# Check for any conflicting operations/arguments
	#--
	# '-l, --all' and '-L, --reallyall' cannot be called together
	if [[ "${all}" == 'true' && "${reallyall}" == 'true' ]]; then
		_error "Running both ${cyan}-l, --all${reset} and ${cyan}-L, --reallyall${reset} conflict!\n" >&2
		_error 'Please choose one or the other.\n' >&2
		exit 1
	fi

	# Store conflicting arguments if '-l, --all' or '-L, --reallyall' was called
	if [[ "${all}" == 'true' || "${reallyall}" == 'true' ]]; then
		# _compress_no_test()
		[[ -n "${operations[3]}" ]] && conflicting_args+=( '-C, --compress_notest' )

		case "${operations[6]}" in
			# _replaygain_apply()
			'replaygain_apply')
				conflicting_args+=( '-G, --replaygain-noforce' )
			;;
		esac

		# Display conflicting arguments and exit, if there were any
		if [[ -n "${conflicting_args[@]}" ]]; then
			# '-l, --all'
			if [[ "${all}" == 'true' ]]; then
				_error "The below options conflict with ${cyan}-l, --all${reset}:\n" >&2
			# '-L, --reallyall'
			elif [[ "${reallyall}" == 'true' ]]; then
				_error "The below options conflict with ${cyan}-L, --reallyall${reset}:\n" >&2
			fi

			# Print each conflicting argument
			for i in "${conflicting_args[@]}"; do
				_error "  ${cyan}${i}${reset}\n" >&2
			done

			_error 'Please remove incompatible options.\n' >&2
			exit 1
		fi
	fi

	# _compress_verify() and _compress_no_test()
	if [[ -n "${operations[2]}" && -n "${operations[3]}" ]]; then
		_error "Running both ${cyan}-c, --compress${reset} and ${cyan}-C, --compress-notest${reset} conflict!\n" >&2
		_error 'Please choose one or the other.\n' >&2
		exit 1
	fi

	# _compress_verify() and _test()
	if [[ -n "${operations[2]}" && -n "${operations[4]}" ]]; then
		_error "Running both ${cyan}-c, --compress${reset} and ${cyan}-t, --test${reset} conflict!\n" >&2
		_error 'Please choose one or the other.\n' >&2
		exit 1
	fi

	# _replaygain_force_apply and _replaygain_apply
	# 'conflict' is set during parameter handling if conflicts will ocurr
	if [[ "${operations[6]}" == 'conflict' ]]; then
		_error "Running both ${cyan}-g, --replaygain${reset} and ${cyan}-G, --replaygain-noforce${reset} conflict!\n" >&2
		_error 'Please choose one or the other.\n' >&2
		exit 1
	fi

	# _aucdtect and _aucdtect w/ spectrogram creation
	# 'conflict' is set during parameter handling if conflicts will ocurr
	if [[ "${operations[0]}" == 'conflict' ]]; then
		_error "Running both ${cyan}-a, --aucdtect${reset} and ${cyan}-A, --aucdtect-spectrogram${reset} conflict!\n" >&2
		_error 'Please choose one or the other.\n' >&2
		exit 1
	fi
}

#-------------------------------------------------------------------------------

_summary()
{
	# Display the summary of operations chart
	#--
	# Set up local variables/arrays
	declare     operation  sub_message
	declare -a  operation_keys

	# Title
	printf "\033[$(_row);2H${blue}%s${reset}\n" \
	'---------------------------------------------------'
	printf '                Summary Of Operations\n'
	printf "${reset} ${blue}%s${reset}\n" \
	'-------------------------+-------------------------'

	# Correct order to display operational status to process
	operation_keys=(
		'Validate with auCDtect'
		'Check MD5 Signature'
		'Compress FLACs'
		'Test FLACs'
		'>> Testing'
		'>> Applying'
		'>> Analyzing'
		'>> Re-Tagging'
		'Extracting Artwork'
		'Prune METADATA Blocks'
	)

	for operation in "${operation_keys[@]}"; do
		# Display each operational line with the status of that operation, if it
		# was called
		#--
		if [[ -n "${operation_summary[$operation]}" ]]; then
			# Check for sub messages, and apply additional formatting
			if [[ "${operation}" == '>> '* ]]; then
				if [[ "${operation}" == '>> Testing' ]]; then
					printf "${yellow}%25s ${blue}|${reset}\n" 'Applying ReplayGain'
				elif [[ "${operation}" == '>> Analyzing' ]]; then
					printf "${yellow}%25s ${blue}|${reset}\n" 'Retagging FLACs'
				fi

				printf -v sub_message '%25s' "${operation}"  # Store right aligned message

				# Color '>>' as yellow and message as magenta
				printf "${yellow}%s${magenta}%s ${blue}|${reset}" \
					"${sub_message%%>> *}>>" "${sub_message##*>>}"
			else
					printf "${yellow}%25s ${blue}|${reset}" "${operation}"
			fi

			# Colorize the operational status
			case "${operation_summary[$operation]}" in
				'Operation Completed')
					printf " ${green}%s${reset}\n" "${operation_summary[$operation]}"
				;;
				'Operation Interrupted')
					printf " ${cyan}%s${reset}\n" "${operation_summary[$operation]}"
				;;
				'Operation Did Not Run')
					printf " ${magenta}%s${reset}\n" "${operation_summary[$operation]}"
				;;
				*'Issue'*)
					printf " ${red}%s${reset}\n" "${operation_summary[$operation]}"
				;;
			esac
		fi
	done

	# Last line of chart
	printf " ${blue}%s${reset}\n" \
		'-------------------------+-------------------------'

	# Remove temporary FIFOs and files
	rm -f "${job_fifo}" "${tmp_aucdtect_fd}" "${issue_ticks}"

	printf '\033[?25h'   # Restore cursor
}

#-------------------------------------------------------------------------------

_run_parallel()
{
	# Run a given operation with a specified number of jobs
	#--
	# $1 is the operational function to run multiple jobs, which can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	#--
	# Set up local variables/arrays
	declare      item
	declare  -i  row_state  placement  previous_placement
	declare  -gi iteration=0

	# Determine the whitespace to make up max length by operation
	case "$1" in
		'replaygain_'* | 'retag_'*)  spacing='11'  ;;
		*)                           spacing='9'   ;;
	esac

	row_state=$(_row)                        # Current cursor row position state

	for item in "${total_items[@]:0:${jobs}}"; do
		# Run as many operations (from $total_items[@]) in the background,
		# specified via $jobs
		#--
		placement=$((row_state + iteration))  # Placement of file/dir processed
		((iteration++))                       # After placement to not print 0
		number_completed="[${iteration}/${#total_items[@]}]"  # eg. [56/213]

		# Max filename allowed (to fit within screen)
		max_length="$(( columns - ${#number_completed} - spacing ))"

		# Fork process
		_$1 "${item}" "$1" "${number_completed}" $((iteration - 1)) &
	done

	# Continue only if there are more items than jobs specified
	if (( ${#total_items[@]} > jobs )); then
		while read -r previous_placement; do
			# An operation is completed with an integer and newline sent to a FIFO.
			# The integer is that row position an operation was on.  For each
			# newline read in, process another file/dir from $total_items[@]
			#--
			# Placement of file/dir relative to previous operation's placement
			placement=$((row_state + previous_placement))

			# If current number of FLACs to process is less than total FLACs
			# found, add another FLAC to process
			if (( iteration < ${#total_items[@]} )); then
				item="${total_items[${iteration}]}"  # Current file/dir to process
				((iteration++))                      # Increase count
				number_completed="[${iteration}/${#total_items[@]}]"  # eg. [56/213]

				# Max filename allowed (to fit within screen)
				max_length="$(( columns - ${#number_completed} - spacing ))"

				# Fork process
				_$1 "${item}" "$1" "${number_completed}" "${previous_placement}" &
			else
				break                         # Prevent read hanging FIFO
			fi
		done <&3                            # Read from FIFO
	fi

	wait  # Wait for children processes
}

#-------------------------------------------------------------------------------

_replaygain_test()
{
	# Test FLAC file's for ReplayGain application
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'sub'

	# Check if file is a FLAC file (capture output) via obtaining the sample
	# rate of the current file.  The sample rate captured will be tested against
	# later on.  Hide STDERR as we'll test the exit code instead
	current_sample_rate="$(metaflac --show-sample-rate "${1}" 2>/dev/null)"

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}" 'sub'

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# File is OK, test if sample rate is above 48kHz and the version of
		# `metaflac' installed is greater than 1.2.1
		if (( current_sample_rate > 48000 )); then

			# Sample rate is greater than 48kHz, so check to make sure the
			# version of `metaflac' is greater than 1.2.1
			if (( $(_metaflac_version) < 3 )); then
				# Old version of `metaflac' installed, display skipped
				_print_status 'skip' "${file_basename}" "${file_length}" 'sub'

				# The `metaflac' version installed is NOT greater than 1.2.1 so
				# skip processing current FLAC file, logging why it was skipped
				printf "%s${unit_separator}FLAC 1.3.0 or higher needed for sample rates >48kHz\n" "${1}" \
					>> "${log_file}"

				printf '.' >> "${issue_ticks}"  # Add one tick to total issues
			else
				# FLAC is ok, display ok
				_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
			fi
		else
			# FLAC is ok, display ok
			_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
		fi
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_replaygain_apply()
{
	# Apply ReplayGain to each directory of FLAC files (if values are missing)
	#--
	# $1 is the directory (includes a slash, ie 'dir/'
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare file_basename percent_complete
	declare -i file_length
	declare -a replaygain_tags

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'sub'

	# ${j} is a FLAC file -> under a directory, ${1}
	for j in "${1}"*${flac_extension}; do
		# Grab all of the ReplayGain tags
		mapfile -n0 -t replaygain_tags < \
			<(metaflac \
				--show-tag='REPLAYGAIN_REFERENCE_LOUDNESS' \
				--show-tag='REPLAYGAIN_TRACK_GAIN' \
				--show-tag='REPLAYGAIN_TRACK_PEAK' \
				--show-tag='REPLAYGAIN_ALBUM_GAIN' \
				--show-tag='REPLAYGAIN_ALBUM_PEAK' \
				"${j}"
			)

		# Test if any ReplayGain values are empty (if there are less
		# than 5 values in the replaygain array)
		if (( ${#replaygain_tags[@]} < 5 )); then
			# At _least_ one tag is missing from current file, so
			# apply new ReplayGain values
			#
			# Add ReplayGain to FLAC files under directory.  Metaflac
			# automatically removes old ReplayGain values (if any) before
			# proceeding
			metaflac --add-replay-gain "${1}"/*${flac_extension} >/dev/null 2>&1

			# Exit code 130 is SIGINT so only check for exit code '1'
			if (( ${?} == 1 )); then
				# Failed applying ReplayGain values, display failed/error
				_print_status 'fail' "${file_basename}" "${file_length}" 'sub'

				# Log ReplayGain error
				printf "%s${unit_separator}Corrupt FLAC(s) or differing sample rates (album ReplayGain)\n" "${1}" \
					>> "${log_file}"

				printf '.' >> "${issue_ticks}"  # Add one tick to total issues
				break                           # Move on to next directory
			else
				# Applied ReplayGain successfully
				_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
			fi
		else
			# All FLACs have ReplayGain applied
			_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
		fi
	done
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_replaygain_force_apply()
{
	# Apply ReplayGain to each directory of FLAC files (force new values)
	#--
	# $1 is the directory (includes a slash, ie 'dir/'
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename  percent_complete
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'sub'

	# Add ReplayGain to FLAC files under directory
	metaflac --add-replay-gain "${1}"*${flac_extension} >/dev/null 2>&1

	# Exit code 130 is SIGINT so only check for exit code '1'
	if (( ${?} == 1 )); then
		# Failed applying ReplayGain values, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}" 'sub'

		# Log ReplayGain error
		printf "%s${unit_separator}Corrupt FLAC(s) or differing sample rates (album ReplayGain)\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# Applied ReplayGain successfully
		_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_compress_verify()
{
	# Compress FLACs with user-defined compression level, verifying its integrity
	#--
	# $1 is the directory
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename percent_complete
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3"

	# Test for COMPRESSION level in FLAC file. Hide error output since
	# we'll be verifying the FLAC file later
	COMPRESSION="$(metaflac --show-tag='COMPRESSION' "${1}" 2> /dev/null)"
	COMPRESSION="${COMPRESSION#*=}"

	if (( COMPRESSION != compression_level )); then
		flac -f -${compression_level} -V "${1}" 2> \
			>(while read -r -d'%' percent_complete; do
				# Compress given FLAC file, verifying with a progress bar
				#--
				# Current percent complete
				percent_complete="$( _get_percent_complete "$2" "${percent_complete}" )"

				# Print operation progress bar and percent complete
				_print_progress "$2" "${percent_complete}" "${file_basename}" "${file_length}"
			done) >/dev/null

		# Exit code 1 implies failure (130 is SIGINT)
		if (( ${?} == 1 )); then
			# Error with FLAC file, display failed/error
			_print_status 'fail' "${file_basename}" "${file_length}"

			# Log FLAC failure
			printf "%s${unit_separator}Failed verification\n" "${1}" \
				>> "${log_file}"

			printf '.' >> "${issue_ticks}"  # Add one tick to total issues
		else
			metaflac \
				--remove-tag='COMPRESSION' \
				--set-tag='COMPRESSION'="${compression_level}" "${1}"

			# FLAC is ok, display ok
			_print_status 'ok' "${file_basename}" "${file_length}"
		fi
	else
		# If already at compression_level, test the FLAC file instead
		flac -t "${1}" 2> \
			>(while read -r -d'%' percent_complete; do
				# Test given FLAC file, with a progress bar
				#--
				# Current percent complete
				percent_complete="$( _get_percent_complete 'test' "${percent_complete}" )"

				# Print operation progress bar and percent complete
				_print_progress 'test' "${percent_complete}" "${file_basename}" "${file_length}"
			done) >/dev/null

		# Exit code 1 implies failure (130 is SIGINT)
		if (( ${?} == 1 )); then
			# Error with FLAC file, display failed/error
			_print_status 'fail' "${file_basename}" "${file_length}"

			# Log FLAC failure
			printf "%s${unit_separator}Failed verification\n" "${i}" \
				>> "${log_file}"

			printf '.' >> "${issue_ticks}"  # Add one tick to total issues
		else
			# FLAC is ok, display ok
			_print_status 'ok' "${file_basename}" "${file_length}"
		fi
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_compress_no_test()
{
	# Compress FLACs with user-defined compression level, verifying its integrity
	# and _not_ falling back to _test() if the compression level is already set
	#--
	# $1 is the directory
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename percent_complete
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3"

	# Test for COMPRESSION level in FLAC file. Hide error output since
	# we'll be verifying the FLAC file later
	COMPRESSION="$(metaflac --show-tag='COMPRESSION' "${1}" 2> /dev/null)"
	COMPRESSION="${COMPRESSION#*=}"

	if (( COMPRESSION != compression_level )); then
		flac -f -${compression_level} -V "${1}" 2> \
			>(while read -r -d'%' percent_complete; do
				# Compress given FLAC file, verifying with a progress bar
				#--
				# Current percent complete
				percent_complete="$( _get_percent_complete "$2" "${percent_complete}" )"

				# Print operation progress bar and percent complete
				_print_progress "$2" "${percent_complete}" "${file_basename}" "${file_length}"
			done) >/dev/null

		# Exit code 1 implies failure (130 is SIGINT)
		if (( ${?} == 1 )); then
			# Error with FLAC file, display failed/error
			_print_status 'fail' "${file_basename}" "${file_length}"

			# Log FLAC failure
			printf "%s${unit_separator}Failed verification\n" "${1}" \
				>> "${log_file}"

			printf '.' >> "${issue_ticks}"  # Add one tick to total issues
		else
			metaflac \
				--remove-tag='COMPRESSION' \
				--set-tag='COMPRESSION'="${compression_level}" "${1}"

			# FLAC is ok, display ok
			_print_status 'ok' "${file_basename}" "${file_length}"
		fi
	else
		# Already at compression_level, print skipped FLAC file
		_print_status 'skip' "${file_basename}" "${file_length}"
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_test()
{
	# Test FLAC file integrity
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename percent_complete
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3"

	flac -t "${1}" 2> \
		>(while read -r -d'%' percent_complete; do
			# Test given FLAC file, with a progress bar
			#--
			# Current percent complete
			percent_complete="$( _get_percent_complete "$2" "${percent_complete}" )"

			# Print operation progress bar and percent complete
			_print_progress "$2" "${percent_complete}" "${file_basename}" "${file_length}"
		done) >/dev/null

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Failed testing\n" "${1}" >> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# FLAC is ok, display ok
		_print_status 'ok' "${file_basename}" "${file_length}"
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_aucdtect()
{
	# Test FLAC validity with auCDtect
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local arrays/variables
	declare    file_basename percent_complete
	declare -i file_length
	declare -a bits_mastering

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3"

	# Get the bit depth and MASTERING tag of a FLAC file.  Also used to check if
	# FLAC file is real.  Hide STDERR output.  The array indices are:
	#   bits_mastering[0] = bit depth (eg, 16)
	#   bits_mastering[1] = MASTERING tag & value (eg, MASTERING=Lossy)
	bits_mastering=( $(metaflac --show-bps --show-tag='MASTERING' "$1" 2>/dev/null) )

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "$1" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues

	# Skip the FLAC file if it has a bit depth greater
	# than 16 since auCDtect doesn't support audio
	# files with a higher resolution than a CD.
	elif (( ${bits_mastering[0]} > 16 )); then
		# Print skipped FLAC
		_print_status 'skip' "${file_basename}" "${file_length}"

		# Log skipped FLAC file
		printf "%s${unit_separator}auCDtect does not support a bit depth >16\n" "$1" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues

	# Skip the FLAC file if it already has the 'Lossy' value set for the
	# MASTERING tag.  This is only done if the value of 'skip_lossy' is 'true',
	# set in the configuration file.  We make sure to remove 'MASTERING=' before
	# testing the tag field
	elif [[ "${bits_mastering[1]#*=}" == 'Lossy' ]]; then
		# Print skipped FLAC
		_print_status 'skip' "${file_basename}" "${file_length}"

		# Log skipped FLAC file
		printf "%s${unit_separator}MASTERING=Lossy value found; skipping ('skip_lossy' configuration)\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues

	# FLAC checks out, continue processing
	else
		# Re-truncate file, since we're doing a multi-stage operation, the
		# filename will be shorter than normal
		IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1" 'multi-stage')

		# Print current FLAC being processed
		_print_item "${file_basename}" "${file_length}" "$3" 'decode'

		# The WAV file to be created from current FLAC file
		decoded_wav="${1%${flac_extension}}_redoflacs_$$.wav"

		flac -d "${1}" -o "${decoded_wav}" 2> \
			>(while read -r -d'%' percent_complete; do
				# Decode FLAC to WAV so auCDtect can read the audio file
				#--
				# Current percent complete
				percent_complete="$( _get_percent_complete 'decode' "${percent_complete}" )"

				# Print operation progress bar and percent complete
				_print_progress 'decode' "${percent_complete}" "${file_basename}" "${file_length}"
			done) >/dev/null

		# Exit code 130 is SIGINT so only check for exit code '1'.  If FLAC file
		# failed decoding to WAV, log error, otherwise continue processing
		if (( ${?} == 1 )); then
			# Error with FLAC file, display failed/error
			_print_status 'fail' "${file_basename}" "${file_length}" 'decode'

			# Log FLAC failure
			printf "%s${unit_separator}Failed decoding to WAV\n" "${1}" \
				>> "${log_file}"

			printf '.' >> "${issue_ticks}"  # Add one tick to total issues
		else
			# Decoded FLAC is ok, display ok
			_print_status 'ok' "${file_basename}" "${file_length}" 'decode'

			# Re-truncate file, since we're doing a multi-stage operation, the
			# filename will be shorter than normal
			IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1" 'multi-stage')

			# Print current FLAC being processed, auCDtect: fast
			_print_item "${file_basename}" "${file_length}" "$3" 'aucdtect_fast'

			# 'export MALLOC_CHECK_' allows the dynamic linked version of
			# `auCDTECT' to run without throwing errors
			export MALLOC_CHECK_='0'

			# The actual auCDtect command with medium accuracy setting (for
			# speed).  STDOUT is sent to file descriptor '4'
			_aucdtect_cmd -m20 "${decoded_wav}" 2> \
				>(while read -r -d'%' percent_complete; do
					# Check FLAC validity by checking decoded WAV via auCDtect (fast)
					#--
					# Current percent complete
					percent_complete="$( _get_percent_complete "$2" "${percent_complete}" )"

					# Print operation progress bar and percent complete
					_print_progress 'aucdtect_fast' "${percent_complete}" "${file_basename}" "${file_length}"
				done) >&4

			# Exit code 130 is SIGINT so only check for exit code '1'
			if (( ${?} == 1 )); then
				# Error with FLAC file, display failed/error
				_print_status 'fail' "${file_basename}" "${file_length}" 'aucdtect_fast'

				# Log FLAC failure
				printf "%s${unit_separator}Failed analyzing decoded FLAC\n" "${1}" \
					>> "${log_file}"

				printf '.' >> "${issue_ticks}"  # Add one tick to total issues
			else
				# Grab the conclusion of auCDtect's command
				# Below options prevents hanging FIFO by only reading
				# what is necessary:
				#    -s7:  Discard first seven lines from auCDtect's output
				#    -n2:  Only grab 2 lines from auCDtect's output
				#     -t:  Remove trailing newlines from auCDtect's output
				#    -u4:  Obtain auCDtect's output from file descriptor '4'
				#  array:  Store captured output into 'aucdtect_check_array'
				mapfile -s7 -n2 -t -u4 aucdtect_check_array

				# If there is an issue with the processed FLAC file, run
				# auCDtect once again with highest setting
				if [[ "${aucdtect_check_array[0]}" != 'This track looks like CDDA with probability 100%' ]]; then
					# Re-truncate file, since we're doing a multi-stage operation, the
					# filename will be shorter than normal
					IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1" 'multi-stage')

					# Print current FLAC being processed, auCDtect: slow
					_print_item "${file_basename}" "${file_length}" "$3" 'aucdtect_slow'

					# The actual auCDtect command with highest accuracy setting.
					# STDOUT is sent to file descriptor '4'
					_aucdtect_cmd -m0 "${decoded_wav}" 2> \
						>(while read -r -d'%' percent_complete; do
							# Check FLAC validity by checking decoded WAV via auCDtect (slow)
							#--
							# Current percent complete
							percent_complete="$( _get_percent_complete "$2" "${percent_complete}" )"

							# Print operation progress bar and percent complete
							_print_progress 'aucdtect_slow' "${percent_complete}" "${file_basename}" "${file_length}"
						done) >&4

					# Exit code 130 is SIGINT so only check for exit code '1'
					if (( ${?} == 1 )); then
						# Error with FLAC file, display failed/error
						_print_status 'fail' "${file_basename}" "${file_length}" 'aucdtect_slow'

						# Log FLAC failure
						printf "%s${unit_separator}Failed analyzing decoded FLAC\n" "${1}" \
							>> "${log_file}"

						printf '.' >> "${issue_ticks}"  # Add one tick to total issues
					else
						# Grab the conclusion of auCDtect's command
						# Below options prevents hanging FIFO by only reading
						# what is necessary:
						#    -s7:  Discard first seven lines from auCDtect's output
						#    -n2:  Only grab 2 lines from auCDtect's output
						#     -t:  Remove trailing newlines from auCDtect's output
						#    -u4:  Obtain auCDtect's output from file descriptor '4'
						#  array:  Store captured output into 'aucdtect_check_array'
						mapfile -s7 -n2 -t -u4 aucdtect_check_array

						# There is an issue with the processed FLAC file
						if [[ "${aucdtect_check_array[0]}" != 'This track looks like CDDA with probability 100%' ]]; then
							# If user specified '-A, --aucdtect-spectrogram', then
							# create a spectrogram with SoX and change logging accordingly
							if [[ "${create_spectrogram}" == 'true' ]]; then
								# Check whether to place spectrogram images in user-defined location
								if [[ -z "${spectrogram_location}" ]]; then
									# Obtain basename of current FLAC file
									flac_file="${1##*/}"

									# Obtain dirname of current FLAC file
									spectrogram_dirname="${1%/*}"

									# Create the spectrogram with '.png' as the
									# file extension, placed in the same
									# directory as the current FLAC file
									spectrogram_picture="${spectrogram_dirname}/${flac_file%${flac_extension}}__${iteration}__.png"
								else
									# Obtain basename of current FLAC file
									flac_file="${1##*/}"

									# Create the spectrogram with '.png' as the
									# file extension, placed in the user-defined
									# location
									spectrogram_picture="${spectrogram_location}/${flac_file%${flac_extension}}__${iteration}__.png"
								fi

								# Re-truncate file, since we're doing a multi-stage operation, the
								# filename will be shorter than normal
								IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1" 'multi-stage')

								# Print current FLAC being processed
								_print_item "${file_basename}" "${file_length}" "$3" 'spectrogram'

								# SoX command to create the spectrogram and
								# place it in spectrogram_picture.  Use the
								# following arguments to create the highest
								# resolution spectrograms:
								#   -x 5000
								#   -y 1025
								_sox_cmd \
									"${decoded_wav}" -S -n spectrogram -c '' -t "${1}" \
									-p 1 -z 90 -Z 0 -q 249 -w Hann -x 1800 -y 513 \
									-o "${spectrogram_picture}" 2> \
										>(while read -r -d'%' percent_complete; do
											# Create spectrogram PNG image of given FLAC
											#--
											# Current percent complete
											percent_complete="$( _get_percent_complete 'spectrogram' "${percent_complete}" )"

											# Print operation progress bar and percent complete
											_print_progress 'spectrogram' "${percent_complete}" "${file_basename}" "${file_length}"
										done) >/dev/null

								# Error creating spectrogram, display issue
								_print_status 'issue' "${file_basename}" "${file_length}" 'spectrogram'

								# Log auCDtect report
								printf "%s${unit_separator}%s (%s)\n" "$1" "${aucdtect_check_array[0]}" "${spectrogram_picture}" \
									>> "${log_file}"

								printf '.' >> "${issue_ticks}"  # Add one tick to total issues
							else
								# Issue with FLAC authenticity, display issue
								_print_status 'issue' "${file_basename}" "${file_length}" 'aucdtect_slow'

								# Log auCDtect report
								printf "%s${unit_separator}%s\n" "$1" "${aucdtect_check_array[0]}" \
									>> "${log_file}"

								printf '.' >> "${issue_ticks}"  # Add one tick to total issues
							fi
						else
							# FLAC is ok, display ok
							_print_status 'ok' "${file_basename}" "${file_length}" 'aucdtect_slow'
						fi
					fi
				else
					# FLAC is ok, display ok
					_print_status 'ok' "${file_basename}" "${file_length}" 'aucdtect_fast'
				fi

				# Remove temporary WAV file
				rm "${decoded_wav}"
			fi
		fi
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_md5_check()
{
	# Check for valid MD5 checksum in FLAC file
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename percent_complete md5_sum
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3"

	# Get the MD5 checksum (hide stderr output).  Also
	# used to check if FLAC file is real
	md5_sum="$(metaflac --show-md5sum "$1" 2>/dev/null)"

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	# FLAC file is real, check for unset MD5 checksum.  We cannot use an
	# arithmetic expression as any amount of 0's will equal the expression
	# below
	elif [[ "${md5_sum}" == '00000000000000000000000000000000' ]]; then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Unset MD5 signature (00000000000000000000000000000000)\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# FLAC is ok, display ok
		_print_status 'ok' "${file_basename}" "${file_length}"
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_retag_analyze()
{
	# Check for missing VORBIS tags from a given FLAC
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'sub'

	# Check if file is a FLAC file (variable hides output)
	check_flac="$(metaflac --show-md5sum "${1}" 2>&1)"

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}" 'sub'

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# Iterate through each tag field and check if tag is missing
		for j in "${tags[@]}"; do
			# Check if ALBUMARTIST is in tag array and apply operations on
			# the tag field if it exists
			if [[ "${j}" == 'ALBUMARTIST' ]]; then
				# ALBUMARTIST exists in tag array so allow script to check the
				# various naming conventions within the FLAC files (ie,
				# 'ALBUM ARTIST' or 'ALBUM_ARTIST')

				# "ALBUMARTIST" or "ALBUM ARTIST" or "ALBUM_ARTIST", case-insensitive
				if [[ -n "$(metaflac --show-tag='ALBUMARTIST' "${1}")" ]]; then
					show_tag_list+=( '--show-tag=ALBUMARTIST' )

				elif [[ -n "$(metaflac --show-tag='ALBUM ARTIST' "${1}")" ]]; then
					show_tag_list+=( '--show-tag=ALBUM ARTIST' )

				elif [[ -n "$(metaflac --show-tag='ALBUM_ARTIST' "${1}")" ]]; then
					show_tag_list+=( '--show-tag=ALBUM_ARTIST' )
				fi
			else
				# Build up metaflac '--show-tag=' list
				show_tag_list+=( "--show-tag=${j}" )
			fi
		done

		# Load up all the tag values for current file
		mapfile -n0 -t metaflac_tag_array < <(metaflac "${show_tag_list[@]}" "${1}")

		# Take above tag values and create an associative
		# array using TAG_FIELD=TAG_VALUE as the key/value pair
		#
		# Specifically declare an empty associative array
		declare -A temp_tag_array

		# Run through the tag array from above and store
		# the values into a temporary tag array
		for tag_field_value in "${metaflac_tag_array[@]}"; do
			# This is the tag field (eg. TITLE)
			tag_field="${tag_field_value%%=*}"

			# Enforce the tag field to be uppercase
			# ${tag_field_value} below is the tag value
			temp_tag_array+=( ["${tag_field^^}"]="${tag_field_value#*=}" )
		done

		# Run through the tags array and test each tag value from
		# ${temp_tag_array} with the currently processed tag in ${tag[@]}.  For
		# each match found, test if the tag value is null, reporting any missing
		# tags. Then remove the tag field (index) from the ${temp_tag_array}, so
		# the next iteration is faster and we can check for missing tags (tag
		# fields)
		for j in "${tags[@]}"; do

			# If the total # of indices in the temporary tag array is above
			# 0, continue testing, else store missing tag (${j}) into the
			# missing tags array
			if (( ${#temp_tag_array[@]} > 0 )); then

				# Compare each tag field from the temporary array to ${j},
				# checking if they are equal and if so, check for missing
				# tag values
				for temp_tag_field in "${!temp_tag_array[@]}"; do
					# Both tag fields are equal
					if [[ "${j}" == "${temp_tag_field}" ]]; then
						# Check if tag value is null, logging missing tags if so
						if [[ -z "${temp_tag_array[${temp_tag_field}]}" ]]; then
							missing_tags+="${j}, "
						fi

						# Remove the current tag field from the temporary tag
						# array since it's been matched already
						unset -v temp_tag_array["${temp_tag_field}"]

						# This variable let's the script know that the current
						# tag, ${j} has been matched up with a tag field in
						# ${temp_tag_array[@]}
						tag_match='true'

						# Break out of loop since the tag fields have been
						# matched, continuing on to the next iteration of ${j}
						break
					fi
				done

				# If there wasn't a tag field in the temporary tag array
				# ( eg. ${temp_tag_array[@]} ) that matched ${j}, the FLAC file
				# must have a missing tag, so log it by throwing the missing tag
				# into the missing tags array
				if [[ "${tag_match}" != 'true' ]]; then
					missing_tags+="${j}, "
				fi

				# Reset the value of tag match (if any) for the next iteration
				# of ${j}
				unset -v tag_match

			# The total # of indices in the temporary tag array is 0, so
			# whatever is left in ${tags[@]} represented by ${j} is logged
			# as missing
			else
				missing_tags+="${j}, "
			fi
		done

		# If missing_tags_array is not empty, there are missing
		# tags in the current file so log output
		if [[ -n "${missing_tags}" ]]; then
			# Error with FLAC file, display failed/error
			_print_status 'fail' "${file_basename}" "${file_length}" 'sub'

			# Log the missing tags
			printf "%s${unit_separator}Missing tags: ${missing_tags%, }\n" "${1}" \
				>> "${log_file}"

			printf '.' >> "${issue_ticks}"  # Add one tick to total issues
		else
			# There are no missing tags, display ok
			_print_status 'ok' "${file_basename}" "${file_length}" 'sub'
		fi
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_retag_apply()
{
	# Re-apply the VORBIS tags specified in the config file, removing all others
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'sub'

	# Remove and set new tags
	#
	# Iterate through the tag array and set a variable for each tag
	for j in "${tags[@]}"; do
		# Check if ALBUMARTIST is in tag array and apply operations on
		# the tag field if it exists
		if [[ "${j}" == 'ALBUMARTIST' ]]; then
			# ALBUMARTIST exists in tag array so allow script to check the
			# various naming conventions within the FLAC files (ie,
			# 'ALBUM ARTIST' or 'ALBUM_ARTIST')
			#--
			# "ALBUMARTIST"
			if [[ -n "$(metaflac --show-tag='ALBUMARTIST' "${1}")" ]]; then
				show_tag_list+=( '--show-tag=ALBUMARTIST' )

			elif [[ -n "$(metaflac --show-tag='ALBUM ARTIST' "${1}")" ]]; then
				show_tag_list+=( '--show-tag=ALBUM ARTIST' )

			elif [[ -n "$(metaflac --show-tag='ALBUM_ARTIST' "${1}")" ]]; then
				show_tag_list+=( '--show-tag=ALBUM_ARTIST' )
			fi
		else
			# Build up metaflac '--show-tag=' list
			show_tag_list+=( "--show-tag=${j}" )
		fi
	done

	# Load up all the tag values for current file
	mapfile -n0 -t metaflac_tag_array < <(metaflac "${show_tag_list[@]}" "${1}")

	# Create a copy of ${metaflac_tag_array[@]} with just the tag fields
	# and force the tag fields to be uppercase
	tag_field_array=( "${metaflac_tag_array[@]%%=*}" )
	tag_field_array=( "${tag_field_array[@]^^}" )

	# Create a copy of ${metaflac_tag_array[@]} with just the tag values
	tag_values_array=( "${metaflac_tag_array[@]#*=}" )

	# Clear the original 'tag_field=tag_value array' for use below
	metaflac_tag_array=()

	# If the user specified 'true' to the 'prepend_zero' option in the
	# configuration file, enforce the TRACKNUMBER and TRACKTOTAL tags to have a
	# '0' prepended before singular numbers
	if [[ "${prepend_zero}" == 'true' ]]; then
		# Prepend a '0' for TRACKNUMBER and TRACKTOTAL
		for j in "${!tag_field_array[@]}"; do
			if [[ "${tag_field_array[${j}]}" == 'TRACKNUMBER' || "${tag_field_array[${j}]}" == 'TRACKTOTAL' ]]; then
				# Remove leading and trailing whitespace in tag value, eg:
				#   '   7     '  ->  '7'
				read -r prepend_tag_value <<< "${tag_values_array[$j]}"

				# Prepend a 0 to the tag value, forcing base10 to prevent octal errors
				printf -v prepend_tag_value '%02d' $((10#${prepend_tag_value}))

				# Store the tag field and tag value into an array, TAG_FIELD=TAG_VALUE
				metaflac_tag_array+=( "${tag_field_array[$j]}=${prepend_tag_value}" )
			else
				# Remove leading and trailing whitespace in tag value, eg:
				#   '   The Black     Halo     '  ->  'The Black     Halo'
				read -r tag_value <<< "${tag_values_array[$j]}"

				# Store the tag field and tag value into an array, TAG_FIELD=TAG_VALUE
				metaflac_tag_array+=( "${tag_field_array[$j]}=${tag_value}" )
			fi
		done
	else
		# ${prepend_zero} is not set as 'true', add each tag field and tag value
		# into ${metaflac_tag_array[@]}
		for j in "${!tag_field_array[@]}"; do
			# Remove leading and trailing whitespace in tag value, eg:
			#   '   The Black     Halo     '  ->  'The Black     Halo'
			read -r tag_value <<< "${tag_values_array[$j]}"

			# Store the tag field and tag value into an array, TAG_FIELD=TAG_VALUE
			metaflac_tag_array+=( "${tag_field_array[$j]}=${tag_value}" )
		done
	fi

	# Add the saved tags back, by printing each tag field and value
	# on a separate line to STDOUT.  This will be read in by metaflac.
	# Use process substitution to allow this to finish if user
	# invokes SIGINT
	metaflac --remove-all-tags --import-tags-from=- "${1}" < \
		<(
			# This prints each tag key and value pair
			# (eg. ARTIST=Kamelot)
			printf '%s\n' "${metaflac_tag_array[@]}"
		)

	# Display ok
	_print_status 'ok' "${file_basename}" "${file_length}" 'sub'

	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_extract_images()
{
	# Extact embedded artwork from a given FLAC
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare -i file_length increment

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'half'

	# Check if file is a FLAC file (hide output)
	metaflac --show-md5sum "${1}" >/dev/null 2>&1

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# Find all the artwork blocks available in the current FLAC file, returned
		# as an array: $artwork[@]
		_find_artwork "$1"

		# Continue only if there were artwork to be extracted
		if [[ -n "${artwork[@]}" ]]; then
			# Determine where to put extracted artwork by checking user config
			if [[ -n "${artwork_location}" ]]; then
				# $artwork_location/file.flac_art
				artwork_directory="${artwork_location}/${1##*/}_art"
			else
				# /path/to/current/flac/file.flac_art
				artwork_directory="${1}_art"
			fi

			# Prevent directory clobbering by creating an incremental version
			until [[ ! -d "${artwork_directory}" ]]; do
				artwork_directory="${artwork_directory%~*~}~$((increment++))~"
			done

			mkdir -p "${artwork_directory}"  # Create artwork directory

			for image in "${artwork[@]}"; do
				# Run through all the artwork found in the current FLAC by looking
				# at $artwork[@], of which, each index looks like:
				#   artwork[0]="${block_id}:${art_id}_${art_desc}.${art_ext}"
				# and extract each image by it's METADATA block number
				#--
				# Prevent file clobbering by creating an incremental version
				increment='1'
				extracted_image="${artwork_directory}/${image#*:}"
				until [[ ! -f "${extracted_image}" ]]; do
					extracted_image="${artwork_directory}/${image#*:}~$((increment++))~"
				done

				# Extract image by the METADATA block number to the art directory
				metaflac \
					--block-number="${image%%:*}" \
					--export-picture-to="${extracted_image}" \
					"$1"
			done
		fi

		# FLAC is ok, display ok
		_print_status 'ok' "${file_basename}" "${file_length}"
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------

_prune()
{
	# Prune given FLAC by removing various BLOCKs (defined in user configuration)
	#--
	# $1 is the filename
	# $2 is the operation that's currently being run, of which, can be:
	#   aucdtect                  md5_check           compress_verify
	#   compress_no_test          test                replaygain_test
	#   replaygain_force_apply    replaygain_apply    retag_analyze
	#   retag_apply               extract_images      prune
	# $3 is a string representing number items processed, eg: [43/439]
	# $4 is the current operation's row placement
	#--
	trap '_kill_jobs "$(jobs -rp)"' EXIT

	# Set up local variables
	declare    file_basename
	declare -i file_length

	# Truncate file, if necessary, and grab file information, splitting on
	# Unit Separator (\037)
	IFS=$'\037' read -r file_basename file_length < <(_truncate_filename "$1")

	# Print current FLAC being processed
	_print_item "${file_basename}" "${file_length}" "$3" 'half'

	# Check if file is a FLAC file (hide output)
	metaflac --show-md5sum "${1}" >/dev/null 2>&1

	# Exit code 1 implies failure (130 is SIGINT)
	if (( ${?} == 1 )); then
		# Error with FLAC file, display failed/error
		_print_status 'fail' "${file_basename}" "${file_length}"

		# Log FLAC failure
		printf "%s${unit_separator}Not a real FLAC file\n" "${1}" \
			>> "${log_file}"

		printf '.' >> "${issue_ticks}"  # Add one tick to total issues
	else
		# Remove all information but STREAMINFO,VORBIS_COMMENTs, and
		# possibly METADATA_BLOCK_PICTURE
		metaflac --remove --dont-use-padding --except-block-type="${dont_prune_flac_metadata}" "${1}"

		# FLAC is ok, display ok
		_print_status 'ok' "${file_basename}" "${file_length}"
	fi
	printf "$4\n" >&3  # Store row position and end operation
}

#-------------------------------------------------------------------------------
# .: PRE-SCRIPT CHECKS :.
#-------------------------------------------------------------------------------
shopt -s globstar                      # BASH 4 globstar (eg, /**/*.flac)

version='0.30'                         # Redoflacs version
flac_extension='.[Ff][Ll][Aa][Cc]'     # FLAC extension (case-insensitive)
unit_separator=$'\037'                 # ASCII unit separator

declare -i script_revision='3'         # Script user configuration revision
declare -i max_length                  # Max filename length within terminal
declare -A operation_summary           # Stores all the operational states

# Colors on by default
reset='\033[0m'
invert='\033[7m'
red='\033[31m'
green='\033[32m'
yellow='\033[33m'
blue='\033[34m'
magenta='\033[35m'
cyan='\033[36m'

# Check if user is running under Cygwin and if so, warn user and exit
os_name="$(uname -o)"
if [[ "${os_name}" == 'Cygwin' ]]; then
	_error 'This version of redoflacs is meant to be run under\n' >&2
	_error "${cyan}UNIX/Linux/BSD${reset}.  Please use the ${cyan}Windows (Cygwin)${reset}\n" >&2
	_error 'version located here:\n' >&2
	_error "${cyan}https://github.com/sirjaren/redoflacsw${reset}\n" >&2
	exit 1
fi

#-------------------------------------------------------------------------------

# Create/source either system or user config
if (( EUID == 0 )); then
	config_file='/etc/redoflacs.conf'               # System (root) config
else
	config_file="${HOME}/.config/redoflacs/config"  # User config
fi

# Check if system-wide configuration doesn't exist
if [[ ! -f "${config_file}" ]]; then
	mkdir -p "${config_file%/*}"  # Create directory if it doesn't exist
	_create_config                # Create configuration file

	_info 'A configuration file has been created here:\n'
	_info "${cyan}${config_file}${reset}\n\n"

	_info 'Please review it before running this program.\n'

	exit 0
else
	_parse_config                 # Parse and source configuration file
fi

#-------------------------------------------------------------------------------

# Generate log file location
#--
# Ensure we don't overwrite an existing log file by incrementing the current PID
# by 1 until the log file that is to be set, doesn't exist
pid="$$"
log_file="${error_log}/redoflacs_${pid}.log"

until [[ ! -f "${log_file}" ]]; do
	log_file="${error_log}/redoflacs_log_$((pid++)).log"
done

#-------------------------------------------------------------------------------

_process_positional_parameters "${@}"  # Process CLI arguments

#-------------------------------------------------------------------------------

# Disable color if user called '-n, --no-color'
[[ "${no_color}" == 'true' ]] && unset -v blue green red cyan magenta yellow

#-------------------------------------------------------------------------------

# Make sure we are running BASH 4 or greater
if (( ${BASH_VERSINFO[0]} < 4 )); then
	_error "You must be running ${cyan}BASH 4${reset} or greater to use\n" >&2
	_error '"this program!\n' >&2
	exit 1
fi

#-------------------------------------------------------------------------------

# Check for any conflicting operations/arguments called and warn user
_check_conflicting_operations

#-------------------------------------------------------------------------------

# For each operation that was selected, add to $operation_summary[@]
for operation in "${operations[@]}"; do
	_update_operation_status "${operation}" 'Operation Did Not Run'
done
#-------------------------------------------------------------------------------

_check_missing_programs  # Check for missing programs vital to this script

#-------------------------------------------------------------------------------

# Check whether directory exists
if [[ ! -d "${directory}" ]]; then
	printf ' Usage: redoflacs [OPTION] [PATH_TO_FLAC(s)]...\n' >&2
	_error 'Please specify a directory!\n' >&2
	exit 1
fi

#-------------------------------------------------------------------------------

# Check for at least 1 FLAC file
read -r find_flacs < <( printf "%s\n" "${directory}"/**/*${flac_extension} )

if [[ ! -f "${find_flacs}" ]]; then
	_error 'There are not any FLAC files to process!\n' >&2
	exit 1
fi

#-------------------------------------------------------------------------------

# If '-e, --extract-artwork' was called, make sure $artwork_location is valid
if [[ "${operations[9]}" == 'extract_images' ]]; then
	# Check if artwork_location is user-defined
	if [[ -n "${artwork_location}" ]]; then
		# Put extracted artwork in user-defined location, testing to make sure
		# the directory exists
		if [[ ! -d "${artwork_location}" ]]; then
			_error "${cyan}${artwork_location}${reset} doesn't exist!\n" >&2
			_error 'Please set a valid directory in the configuration file!\n' >&2
			exit 1
		fi
	fi
fi

# If '-A, --aucdtect-spectrogram' was called, make sure $spectrogram_location is
# valid
if [[ "${create_spectrogram}" == 'true' ]]; then
	# Check if spectrogram_location is user-defined
	if [[ -n "${spectrogram_location}" ]]; then
		# Put spectrograms in user-defined location, testing to make sure the
		# directory exists
		if [[ ! -d "${spectrogram_location}" ]]; then
			_error "${cyan}${spectrogram_location}${reset} doesn't exist!\n" >&2
			_error 'Please set a valid directory in the configuration file!\n' >&2
			exit 1
		fi
	fi
fi

#-------------------------------------------------------------------------------

# If $jobs wasn't set on invocation, check in /proc determine the number of jobs
if [[ -z "${jobs}" ]]; then
	_find_cores
else
	jobs_display='(User Defined)'
fi

#-------------------------------------------------------------------------------
# .: START SCRIPT :.
#-------------------------------------------------------------------------------
old_stty="$(stty -g)"              # Store current stty settings
stty -ctlecho 2>/dev/null          # Disable ctrl+c '^C'
printf '\033[?25l'                 # Hide cursor
_check_config_version              # Check if script config is newer than user's
job_fifo="/tmp/redoflacs_fifo_$$"  # Job manager FIFO

tmp_aucdtect_fd="/tmp/redoflacs_aucdtect_fifo_$$"    # auCDtect's STDOUT FIFO
trap 'rm -f "${job_fifo}" "${tmp_aucdtect_fd}"' EXIT # Allow for FIFO removal
mkfifo "${tmp_aucdtect_fd}"                          # Create auCDtect FIFO
exec 4<>"${tmp_aucdtect_fd}"                         # Open read/write on fd '4'

issue_ticks="/tmp/redoflacs_issue_file_$$"           # Issues/Errors file

# _retag_apply: Display a countdown
[[ -n "${operations[8]}" ]] && _countdown_metadata

_top_banner                                          # Display top banner
total_items=( "${directory}"/**/*${flac_extension} ) # Total FLACs to process

# This displays '[ ok ]' after 'Finding FLAC files to process...'
printf "%11s${blue}[ ${green}ok${blue} ]${reset}\n" ''

#-------------------------------------------------------------------------------

for operation in "${operations[@]}"; do
	# Loop through the various operations to run.  All the operations are to be
	# run, in order, which is detailed below:
	#   operations[0]='aucdtect'
	#   operations[1]='md5_check'
	#   operations[2]='compress_verify'
	#   operations[3]='compress_no_test'
	#   operations[4]='test'
	#   operations[5]='replaygain_test'
	#   operations[6]='replaygain_force_apply'   # Conditionally set
	#   operations[6]='replaygain_apply'         # Conditionally set
	#   operations[7]='retag_analyze'
	#   operations[8]='retag_apply'
	#   operations[9]='extract_images'
	#   operations[10]='prune'
	#--
	case "${operation}" in
		'replaygain_'*'apply')
			total_items_backup=( "${total_items[@]}" )  # Total FLACs backup array
			_get_directory_list "${total_items[@]}"     # Returns $total_dirs[@]
			total_items=( "${total_dirs[@]}" )          # Now process directories
		;;
		'prune')
			# Set which FLAC metadata BLOCKS to remove
			if [[ "${remove_artwork}" == 'true' ]]; then
				# Remove PICTURE block
				dont_prune_flac_metadata='STREAMINFO,VORBIS_COMMENT'
			else
				# Do not remove PICTURE block
				dont_prune_flac_metadata='STREAMINFO,PICTURE,VORBIS_COMMENT'
			fi
		;;
	esac

	_message "${operation}"        # Display title message of current operation
	_scroll_terminal "$(_row)"     # Scroll terminal up (if needed)
	_clear_jobs_fd "${job_fifo}"   # Clear job manager file descriptor (3)

	if (( ${#total_items[@]} < jobs )); then
		# Set row position to place cursor after completing an operation or trap
		#--
		# If the items to process are less than the number of jobs, add the number
		# of items to the current row position, else add the number of jobs to run
		items_processed="${#total_items[@]}"
	else
		items_processed="${jobs}"
	fi

	post_row=$(( $(_row) + items_processed ))  # After operation row position

	trap '_trap_sigint "${operation}"' SIGINT  # Clean exit on SIGINT
	_run_parallel "${operation}"   # Run a given operation with parallel jobs

	for (( i=1; i<=items_processed; i++)); do
		# Clear all the operation lines by moving up each line and clearing it,
		# until we are just below the operation's title message
		#--
		printf "\033[$(( post_row - i))H%${columns}s" ''
	done

	# Display number of finished items minus items with issues
	_message "${operation}" "$(( iteration - $(_num_issues) ))"  # Update title message

	# Update status for the current operation
	_update_operation_status "${operation}" 'Operation Completed'

	if [[ -f "${log_file}" ]]; then
		_message_log_exists "${operation}"  # Print out log exists to STDERR
		_create_log "${operation}"          # Create and format log
		_summary                            # Display Summary Of Operations
		exit 1
	fi

	# Restore the items to process from directories to FLACs
	[[ "${operation}" == 'replaygain_'*'apply' ]] && total_items=( "${total_items_backup[@]}" )
done

#-------------------------------------------------------------------------------

_summary  # Display Summary Of Operations
