Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 3903 lines (3384 sloc) 137 KB
#!/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-2015 Jaren Stangret (and contributors)
#-------------------------------------------------------------------------------
# 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.
.P
\fBNOTE\fP: If this operation is called with '\fB\-x, \-\-no-extra-tags\fP', the above
logic changes. Instead of checking the \fBCOMPRESSION\fP tag before continuing,
each FLAC file is ALWAYS compressed with the configured compression level. After
successful compression, the \fBCOMPRESSION\fP tag is NOT applied to the FLAC
file.
.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.
.P
\fBNOTE\fP: This operation is NOT compatible with the '\fB\-x, \-\-no-extra-tags\fP'
option. This option invalidates the usefulness of this operation since each
FLAC file that is missing the \fBCOMPRESSION\fP tag would be compressed, after
which, the \fBCOMPRESSION\fP tag would NOT be applied, leaving future executions
of this operation to repeat this process.
.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 \-x, \-\-no-extra-tags
.RS
Disable the application of extra tags. Presently, the only tag that's applied
by default is the \fBCOMPRESSION\fP tag during compression via the '\fB\-c, \-\-compress\fP'
option.
.P
This option has the effect of invalidating the '\fB\-C, \-\-compress-notest\fP'
operation, making the invocation of these two arguments together incompatible.
See the '\fB\-c, \-\-compress\fP' and '\fB\-C, \-\-compress-notest\fP'
operations for more information on how this option affects these operations.
.P
This option has no bearing on the '\fB\-r, \-\-retag\fP' operation. All FLAC
tags defined in the configuration file are for use with the '\fB\-r, \-\-retag\fP'
operation only and are not subject to, or affected by, the '\fB\-x, \-\-no-extra-tags\fP'
option.
.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
\fB6\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 ' -x, --no-extra-tags\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')
message='Prune METADATA Blocks'
;;
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"
# PRESERVE FILE MODIFICATION TIME
#--
# This is enabled by setting 'preserve_modtime' option as 'true'. All other
# values are interpreted as 'false'
preserve_modtime="false"
## CONFIG REVISION 4 :: 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}"
if [[ "${preserve_modtime}" == 'true' ]]; then
metaflac_extra_options="--preserve-modtime"
fi
}
#-------------------------------------------------------------------------------
_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
# 'nproc' is part of GNU coreutils
if type -P nproc >/dev/null; then
jobs="$(nproc)" # Store useable 'online' CPU's
jobs_display='(nproc)' # $jobs dynamically determined
return 0
fi
# Fallback to /proc/cpuinfo. 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
jobs='0' # Initialize to zero
# /proc/cpuinfo exists, find total number of cores to use by countine the
# number of processor entries are in /proc/cpuinfo
while read -r i; do
[[ "${i}" == 'processor'*:' '* ]] && ((jobs++))
done < /proc/cpuinfo
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
while read -r; do
# We only care about the block, picture and MIME type lines
[[ "${REPLY}" == 'METADATA'* || "${REPLY}" == ' '[tM]* ]] && artwork_blocks+=( "${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_extra_tags 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 ) ;;
'--no-extra-tags') converted_args+=( -x ) ;;
'--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:LlcCtgGaAmeprnxhvo" 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 ) ;;
'x') converted_args+=( -x ) ;;
'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' ;;
'-x') no_extra_tags='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\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 '\nPlease 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\n" >&2
_error 'Please choose one or the other.\n' >&2
exit 1
fi
# _compress_no_test() and '-x, --no-extra-tags'
if [[ -n "${operations[3]}" && "${no_extra_tags}" == 'true' ]]; then
_error "Running both ${cyan}-C, --compress-notest${reset} and ${cyan}-x, --no_extra_tags${reset} conflict!\n\n" >&2
_error "${cyan}-x, --no_extra_tags${reset} invalidates the compression check, since each file\n" >&2
_error "seen to be missing the ${cyan}COMPRESSION${reset} tag is compressed, after which, the\n" >&2
_error "${cyan}COMPRESSION${reset} tag is NOT applied, leaving future executions to repeat this process.\n\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\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\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\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 ${metaflac_extra_options} --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 ${metaflac_extra_options} --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 FLAC file operated on
# $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"
# Only obtain current COMPRESSION value if the user did not specify '-x,
# --no-extra-tags'
if [[ "${no_extra_tags}" != 'true' ]]; then
# 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#*=}"
fi
if (( COMPRESSION != compression_level )) || [[ "${no_extra_tags}" == 'true' ]]; 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
# Only remove/add the new compression level if the user did not specify
# no additional tags ('-x, --no-extra-tags' option)
if [[ "${no_extra_tags}" != 'true' ]]; then
metaflac ${metaflac_extra_options} \
--remove-tag='COMPRESSION' \
--set-tag='COMPRESSION'="${compression_level}" "${1}"
fi
# 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 FLAC file operated on
# $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 ${metaflac_extra_options} \
--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 "${metafla