You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2312 lines
106 KiB
2312 lines
106 KiB
#!/usr/bin/env bash
|
|
# shellcheck disable=SC1090,SC1112,SC2312
|
|
# SC1090=ignore 'Can't follow non-constant source' when sourcing config (the config is still shellchecked at script start)
|
|
# SC1112=ignore unicode quote (this is an erroneous warning for unicode quote chars used in expressions)
|
|
# SC2312=Consider invoking this command separately
|
|
set -h -u -o 'pipefail'
|
|
shopt -u 'nocaseglob' 'nocasematch' 'nullglob' # just for safety, plus we need to do case-sensitive matching
|
|
|
|
## SCRIPT INFORMATION
|
|
# ------------------------------------------------------------------------------
|
|
# Name : Muzik Faktry
|
|
# License : Non-Profit Open Software License ("Non-Profit OSL") 3.0
|
|
# Requirements : GNU/Linux, Bash compatible shell
|
|
# Dependancies : primary: ffmpeg, flac, mapfile, mediainfo, pcre2, rsgain,
|
|
# : shntool
|
|
# : optional: libnotify, shellcheck
|
|
# Author : 12bytes.org
|
|
# Website : https://12bytes.org/projects/muzik-faktry/
|
|
# Code : https://codeberg.org/12bytes.org/muzikfaktry
|
|
#
|
|
## LICENSE
|
|
# ------------------------------------------------------------------------------
|
|
# This program is free software as afforded by the Non-Profit Open Software
|
|
# License ("Non-Profit OSL") 3.0, a copy of which should have been included. The
|
|
# license can be viewed on-line at: https://opensource.org/licenses/NPOSL-3.0
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# BEGIN DEVELOPER NOTES
|
|
|
|
## TO-DO ITEMS
|
|
# ------------------------------------------------------------------------------
|
|
# TODO for finding dupe files, use hashes of the audio data
|
|
#
|
|
## NOTES
|
|
#------------------------------------------------------------------------------
|
|
# because i always forget...
|
|
# err to out: 2>&1
|
|
# supress out: > '/dev/null'
|
|
# supress err: 2> '/dev/null'
|
|
# suppress all: &> '/dev/null' (or) > '/dev/null' 2>&1 (the former requires Bash 4+)
|
|
#
|
|
# Vorbis comment tags spec: https://xiph.org/vorbis/doc/v-comment.html
|
|
# ALBUM ARTIST COMMENT CONTACT COPYRIGHT DATE DESCRIPTION GENRE ISRC LICENSE LOCATION ORGANIZATION PERFORMER TITLE TRACKNUMBER VERSION
|
|
#
|
|
# one-liner for generating spectrograms for a folder of files
|
|
# $ for file in *.flac; do ffmpeg -hide_banner -i "${file}" -lavfi 'showspectrumpic=size=1024x512' "${file}.png"; done
|
|
#
|
|
# shntool:
|
|
# don't do cd quality check by default since files that fail may be higher than CD quality - rely on sample/bit rates
|
|
# flac files fialing cd quality tests often/always return 'yes' for the 'Non-canonical header check'
|
|
# to get strings returned by shntool see: shntool-3.0.10/src/mode_info.c
|
|
# "Shntool does not provide any meaningful information on whether a FLAC is damaged":
|
|
# https://openpreservation.org/blogs/breaking-waves-and-some-flacs/
|
|
# flac and ffmpeg do integrity checking however, including comparing MD5 hash of audio stream to the stored value
|
|
#
|
|
# don't bother genre tagging using music BPM info - way too unreliable
|
|
#
|
|
# don't bother with 'lossless audio checker'
|
|
#
|
|
# pure bash bible: https://github.com/dylanaraps/pure-bash-bible#cycle-through-an-array
|
|
# Writing Safe Shell Scripts: https://sipb.mit.edu/doc/safe-shell/
|
|
# POSIX RegEx: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04
|
|
# PCRE: https://www.pcre.org/current/doc/html/pcre2pattern.html
|
|
# Audio file formats: https://en.wikipedia.org/wiki/Audio_file_format
|
|
# Wave File Format: https://wavefilegem.com/how_wave_files_work.html
|
|
# Detailed explanation of WAV file format: https://www.fatalerrors.org/a/detailed-explanation-of-wav-file-format.html
|
|
# more wave format: https://www.fatarg/a/detailed-explanation-of-wav-file-format.html
|
|
# FLAC: https://www.thewelltemperedcomputer.com/KB/Flac.htm
|
|
#
|
|
# aError() codes
|
|
# code printed prefix description
|
|
# -------------------------------------
|
|
# [ERR] 'ERROR' a program failed to process the file, usually an unrepairable problem - in batch mode the file is moved to /discard
|
|
# [WRN] 'WARNING' failed a test, or a program failed to process the file - may be repairable - in batch mode the file is moved to /discard
|
|
# [NTC] 'NOTICE' a repairable problem - in batch mode the file is moved to /discard
|
|
# [TAG] 'METADATA' a repairable metdata issue - in batch mode the file is moved to /discard
|
|
# [INF] N/A informational only
|
|
# [USR] N/A file discarded by user and moved to /discard
|
|
# [PRN] N/A prints an error message to the terminal as ususal, but the file is not discarded in batch mode
|
|
#
|
|
# END DEVELOPER NOTES
|
|
|
|
# BEGIN TEXT FORMATTING AND COLORS
|
|
|
|
# WARNING: IF CHANGING THESE VALUES, BE SURE TO SHELLCHECK THE SCRIPT BEFORE
|
|
# RUNNING IT. if you have shellcheck installed:
|
|
# $ shellcheck -o all muzikfaktry.sh
|
|
# otherwise check it online at: https://www.shellcheck.net/
|
|
# your terminal color theme may override the configured colors
|
|
# for formatting options see:
|
|
# https://misc.flogisoft.com/bash/tip_colors_and_formatting
|
|
# https://www.ditig.com/256-colors-cheat-sheet
|
|
#
|
|
# text background color format
|
|
# ------------------------------------------
|
|
sFmtTitle='\e[94;40m' # light blue black (16 colors)
|
|
sFmtTitle2='\e[92;40m' # light green black (16 colors)
|
|
sFmtMenu='\e[30;42m' # black green (16 colors)
|
|
sFmtHeader='\e[97;100m' # white dark gray (16 colors)
|
|
sFmtQuestion='\e[97;45m' # white magenta (16 colors)
|
|
sFmtInfo='\e[97;44m' # white blue (16 colors)
|
|
sFmtSuccess='\e[30;102m' # black light green (16 colors)
|
|
sFmtNotice='\e[30;103m' # black light yellow (16 colors)
|
|
sFmtWarning='\e[30;48;5;208m' # black orange (256 colors)
|
|
sFmtError='\e[30;41m' # black red (16 colors)
|
|
sFmtEnd='\e[0m' # do not edit
|
|
|
|
# END TEXT FORMATTING AND COLORS
|
|
|
|
# BEGIN MISC. GLOBALS
|
|
|
|
sScriptName='Muzik Faktry'
|
|
sScriptVer='20230309'
|
|
sWebPg='https://12bytes.org/projects/muzik-faktry/'
|
|
sRepoURL='https://codeberg.org/12bytes.org/muzikfaktry'
|
|
sReleaseNotesRawURL='https://codeberg.org/12bytes.org/muzikfaktry/raw/branch/main/release_notes.txt'
|
|
sPkgURL='https://codeberg.org/12bytes.org/muzikfaktry/archive/main.zip'
|
|
sConfig='../config/default.conf'
|
|
|
|
aFileExt=() # array of file extensions used to determine which files in /working to process
|
|
sCompletedTasks='' # holds names of all completed tasks for display in main loop
|
|
sLastFile='' # holds name of last file processed for display in main loop
|
|
iDev=0 # developer mode
|
|
iCalcFileSz=0 # whether to show file size stats
|
|
iDiskFreeChk=0 # whether to check free disk space
|
|
sIllegalFileCharsRE='[][*$|\/\\]' # characters that file names may not contain
|
|
|
|
# task names
|
|
sTask_Decompress='Decompress'
|
|
sTask_EditFileName='Edit File Names'
|
|
sTask_Encode='Encode To FLAC'
|
|
sTask_FileAttribs='Set File Permissions'
|
|
sTask_FileInfo='File Information'
|
|
sTask_FileNameToTags='File Name To Tags'
|
|
sTask_FindDupes='Find Duplicates'
|
|
sTask_FormatFileName='Format File Names'
|
|
sTask_Gain='Normalize Gain'
|
|
sTask_Integ1='Integrity Check 1'
|
|
sTask_Integ2='Integrity Check 2'
|
|
sTask_Optimize='Optimize'
|
|
sTask_Play='Play'
|
|
sTask_RestoreTags='Restore Metadata'
|
|
sTask_SaveTags='Save Metadata'
|
|
sTask_Spectrogram='Spectrogram'
|
|
sTask_SplitAlbum='Split An Album'
|
|
sTask_StripMeta='Strip Metadata'
|
|
sTask_TagsToFileName='Tags To File Name'
|
|
sTask_TrimSilence='Trim Silence'
|
|
sTask_WriteTag='Write Tags'
|
|
|
|
# END MISC. GLOBALS
|
|
|
|
# get script arguments
|
|
while getopts 'dhrw' s ; do
|
|
case "${s}" in
|
|
( 'd' ) iDev=1 ; break ;;
|
|
( 'h' )
|
|
a=(
|
|
''
|
|
'Usage: muzikfaktry.sh [-d] [-h] [-r] [-w]'
|
|
''
|
|
'Where:'
|
|
' -d Developer mode'
|
|
' -h Display this help text'
|
|
' -r Read the help file'
|
|
' -w Visit the Muzik Faktry web page'
|
|
)
|
|
printf '%s\n' "${a[@]}"
|
|
;;
|
|
( 'r' ) xdg-open 'README.md' &> '/dev/null' ;;
|
|
( 'w' ) xdg-open "${sWebPg}" &> '/dev/null' ;;
|
|
( * ) printf '%s\n' 'Invalid option. Run with -h for help.' ; exit 1 ;;
|
|
esac
|
|
exit
|
|
done
|
|
|
|
f_scriptInit () {
|
|
|
|
local a=() i s sRet sRE
|
|
|
|
if [[ "${iDev}" -eq 0 ]] ; then
|
|
# warn if terminal width is too small
|
|
i="$( tput 'cols' )"
|
|
if [[ "${i}" -lt 80 ]] ; then
|
|
a=(
|
|
"${sFmtInfo} ${sScriptName} will look better if you make your terminal window wider. ${sFmtEnd}"
|
|
"Currently it is ${i} columns wide and it should be at least 80."
|
|
'' '<--- To help you set the width this line is exactly 80 characters in length --->' ''
|
|
"${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
fi
|
|
|
|
clear
|
|
a=(
|
|
''
|
|
"${sFmtTitle} ------------------------------------------- "
|
|
"${sFmtTitle2} M U Z I K F A K T R Y ${sFmtTitle} by 12bytes.org "
|
|
" ------------------------------ v${sScriptVer} -- ${sFmtEnd}"
|
|
'' "This software is licensed under the Non-Profit Open Software License v3.0." ''
|
|
'To learn more see:' ''
|
|
' Muzik Faktry: Processing music files on Linux'
|
|
" ${sFmtTitle}${sWebPg}${sFmtEnd}" ''
|
|
'Press Ctrl+S to pause the script and Ctrl+Q to resume again. To forcefully'
|
|
'terminate the script you may press Ctrl+C, however it is always best to exit'
|
|
'using the Quit function available in the Main Menu.' ''
|
|
'Run the script with '\''-h'\'' to see the available options.' ''
|
|
"${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
clear
|
|
fi
|
|
|
|
printf '%b\n' "${sFmtHeader} Initializing... ${sFmtEnd}"
|
|
|
|
# create directories
|
|
printf '%s' 'Check/create directories: '
|
|
a=(
|
|
'backup' 'bin' 'config' 'discard/unrepairable' 'discard/serious_issues'
|
|
'discard/minor_issues' 'discard/metadata_issues' 'discard/user_discard'
|
|
'finished' 'holding' 'logs' 'metadata' 'spectro' 'working'
|
|
)
|
|
for s in "${a[@]}" ; do
|
|
if [[ ! -d "${s}/" ]] ; then
|
|
if ! mkdir --parents -- "${s}/" ; then
|
|
printf '%b\n' "${sFmtError} Failed to create directory: ${s} ${sFmtEnd}" ; exit 1
|
|
fi
|
|
fi
|
|
done
|
|
printf '%s\n' 'OK'
|
|
|
|
# set working directory
|
|
printf '%s' 'Set working directory: '
|
|
if ! cd -- 'working' ; then
|
|
printf '%b\n' "${sFmtError} Failed to set working directory ${sFmtEnd}" ; exit 1
|
|
fi
|
|
printf '%s\n' 'OK'
|
|
|
|
# manage log files after which we can begin logging - needs to be after we set working dir
|
|
printf '%b\n' "${sFmtHeader} Housekeeping... ${sFmtEnd}"
|
|
f_houseKeeping 'openSession'
|
|
|
|
printf '%b\n' "${sFmtHeader} Initializing... ${sFmtEnd}"
|
|
# add /bin directory to $PATH so we can run any user included deps
|
|
printf '%s' 'Adding /bin to PATH: '
|
|
if ! export PATH="${PATH}:../bin" ; then
|
|
printf '%b\n' "${sFmtError} Failed to export path ${sFmtEnd}" ; exit 1
|
|
fi
|
|
printf '%s\n' 'OK'
|
|
|
|
# check for dependancies - needs to be after we add /bin to $PATH
|
|
a=()
|
|
printf '%s' 'Dependency check: '
|
|
type 'ffmpeg' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} ffmpeg (used for multiple operations)" )
|
|
type 'ffplay' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} ffplay (used for playing video spectrograms)" )
|
|
type 'flac' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} flac (for working with FLAC files)" )
|
|
type 'metaflac' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} metaflac (for working with FLAC files)" )
|
|
type 'rsgain' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} rsgain (for normalizing gain)" )
|
|
type 'mapfile' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} mapfile (used for multiple operations)" )
|
|
type 'mediainfo' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} mediainfo (used for multiple operations)" )
|
|
if ! type 'notify-send' &> '/dev/null' ; then # provided by 'libnotify'
|
|
a+=( "${sFmtNotice} Missing optional dependency: ${sFmtEnd} libnotify (provides desktop notifications)" )
|
|
sNotify='disable'
|
|
fi
|
|
type 'pcre2grep' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} pcre2 (used for multiple operations)" )
|
|
type 'shellcheck' &> '/dev/null' || a+=( "${sFmtWarning} Missing recommended dependency: ${sFmtEnd} shellcheck (used to check syntax of config files)" )
|
|
type 'shntool' &> '/dev/null' || a+=( "${sFmtError} Missing required dependency: ${sFmtEnd} shntool (used for multiple operations)" )
|
|
|
|
if [[ "${#a[@]}" -gt 0 ]] ; then
|
|
printf '%b\n' "${a[@]}"
|
|
if [[ "${a[*]}" = *'required dependency'* ]] ; then
|
|
printf '%b\n' "${sFmtError} Unable to continue ${sFmtEnd}"
|
|
exit 1
|
|
else
|
|
printf '%b\n' "${sFmtQuestion} Continue without installing missing package(s)? [y/N] ${sFmtEnd}"
|
|
read -rsN1
|
|
[[ "${REPLY}" != 'y' ]] ; exit
|
|
fi
|
|
else
|
|
printf '%s\n' 'OK'
|
|
fi
|
|
|
|
# create some files
|
|
printf '%s' 'Check/create files: '
|
|
if ! touch '../duplicates.txt' ; then
|
|
printf '\n%b\n' "${sFmtError} Failed to create duplicates.txt file ${sFmtEnd}" ; exit 1
|
|
fi
|
|
printf '%s\n' 'OK'
|
|
|
|
# load configuration file - needs to be after we have a log file and before we check for updates
|
|
printf '%s' 'Read cofiguration: '
|
|
|
|
# check that a config file exists and ask which to load if more than 1
|
|
# we always load the default settings since this provides the user with the option
|
|
# of having a config file containing only options which they want to override
|
|
f_defaultSettings
|
|
a=( '../config/'*'.conf' )
|
|
if [[ -z "${a[*]}" ]] ; then
|
|
printf '%b\n' "${sFmtNotice} No configuration file found, using default settings ${sFmtEnd}"
|
|
elif [[ "${#a[@]}" -gt 1 ]] ; then
|
|
printf '\n%b\n' "${sFmtQuestion} Select a configuration file ${sFmtEnd}"
|
|
select sConfig in "${a[@]}"
|
|
do
|
|
break
|
|
done
|
|
fi
|
|
|
|
## shellcheck and source the config file if one exists
|
|
if [[ -f "${sConfig}" ]] ; then
|
|
if ! type 'shellcheck'>'/dev/null' ; then
|
|
printf '%b\n' "${sFmtWarning} '${sConfig}' was not checked for errors (shellcheck not found) ${sFmtEnd}"
|
|
elif ! shellcheck --shell='bash' --enable='all' "${sConfig}" ; then
|
|
a=(
|
|
"${sFmtError} Syntax check for '${sConfig}' file failed! ${sFmtEnd}"
|
|
"Any errors in ${sConfig} must be fixed before continuing"
|
|
)
|
|
printf '%b\n' "${a[@]}" ; exit 1
|
|
else
|
|
printf '%s\n' 'OK'
|
|
fi
|
|
# load the config
|
|
if ! source "${sConfig}" ; then
|
|
printf '%b\n' "${sFmtError} Failed to source '${sConfig}' ${sFmtEnd}" ; exit 1
|
|
fi
|
|
fi
|
|
|
|
# print global settings
|
|
printf '%b\n' "${sFmtHeader} Global Options ${sFmtEnd}"
|
|
if [[ "${iDev}" -eq 0 ]] ; then
|
|
printf '%s\n' 'Developer mode : disabled'
|
|
else
|
|
printf '%b\n' "Developer mode : ${sFmtWarning} ENABLED ${sFmtEnd}"
|
|
fi
|
|
a=(
|
|
"Configuration file : ${sConfig}"
|
|
"Auto-discard : ${sAutoDiscard}"
|
|
"Min. free disk space : ${iDiskFree} MB"
|
|
"Desktop notifications : ${sNotify}"
|
|
)
|
|
printf '%s\n' "${a[@]}"
|
|
|
|
if [[ "${iDev}" -eq 1 ]] ; then
|
|
printf '%b\n' "Check for updates : ${sFmtWarning} DISABLED ${sFmtEnd}"
|
|
elif [[ "${sScriptUpdate}" = 'enable' ]] ; then
|
|
a=(
|
|
"Check for updates : ${sScriptUpdate}"
|
|
"${sFmtHeader} Checking For Update... ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
if sRet="$( wget --quiet --https-only --timeout '15' --output-document - "${sReleaseNotesRawURL}" )" ; then
|
|
sRE='^RELEASE NOTES FOR MUZIK FAKTRY v([0-9]+)' # match version string from release notes
|
|
if [[ "${sRet}" =~ ${sRE} ]] ; then
|
|
s="${BASH_REMATCH[1]}"
|
|
if [[ "${sScriptVer}" != "${s}" ]] ; then
|
|
a=(
|
|
"${sFmtInfo} ${sScriptName} v${s} is available ${sFmtEnd}"
|
|
"${sFmtQuestion} Download the new version? [Y/n] ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" != 'n' ]] ; then
|
|
if wget --https-only --timeout '15' --output-document "../muzikfaktry_v${s}.zip" "${sPkgURL}" && [[ -f "../muzikfaktry_v${s}.zip" ]] ; then
|
|
a=(
|
|
"${sFmtSuccess} Download complete! ${sFmtEnd}"
|
|
"Exiting so you can unpack 'muzikfaktry_v${s}.zip'"
|
|
)
|
|
printf '%b\n' "${a[@]}" ; exit
|
|
else
|
|
printf '%b\n' "${sFmtError} Download failed ${sFmtEnd}"
|
|
fi
|
|
fi
|
|
else
|
|
printf '%s\n' 'You already have the latest version'
|
|
fi
|
|
else
|
|
printf '%b\n' "${sFmtError} Failed to acquire remote version ${sFmtEnd}"
|
|
fi
|
|
else
|
|
printf '%b\n%s\n' "${sFmtWarning} Update check failed ${sFmtEnd}" "${sRet}"
|
|
fi
|
|
printf '%b\n' "${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
read -rsN1
|
|
fi
|
|
|
|
f_mainLoop
|
|
}
|
|
|
|
f_taskInit () {
|
|
|
|
local a=()
|
|
|
|
[[ "${iDev}" -eq 0 ]] && clear
|
|
|
|
case "${sTask}" in
|
|
|
|
( "${sTask_FileAttribs}" )
|
|
aFileExt=( 'n/a' ) # array of file extensions suitable for the task
|
|
iCalcFileSz=0 # whether we compare in/out file size for tasks which may alter the size
|
|
iDiskFreeChk=0 # whether to check free disc space
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"File permissions : ${iChmodOpt}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_SplitAlbum}" )
|
|
aFileExt=( 'aif' 'aiff' 'alac' 'als' 'ape' 'flac' 'lpac' 'ofr' 'shn' 'wav' 'wv' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=1
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"File name format : ${aSplitShntoolOpt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_SaveTags}" )
|
|
aFileExt=( 'aac' 'alac' 'ape' 'flac' 'mp1' 'mp2' 'mp3' 'mpc' 'ogg' 'opus' 'ra' 'wav' 'wma' 'wv' ) # mostly untested
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=1
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Tags to save : ${aSaveTags[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_TagsToFileName}" )
|
|
aFileExt=( 'flac' ) # we could handle all that $sTask_SaveTags can
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"File name format : ${aTagToFileFormat[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_FormatFileName}" )
|
|
aFileExt=( 'n/a' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Character convert : ${sFmtFileNameIconv}"
|
|
"Require ASCII : ${sFmtFileNameAscii}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
a=(
|
|
'Expressions:'
|
|
"${aFmtFileNameRE[@]}"
|
|
)
|
|
printf '%s\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_EditFileName}" )
|
|
aFileExt=( 'n/a' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"${sFmtInfo} This task will be run in batch mode ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Decompress}" )
|
|
# shntool formats:
|
|
# 'aiff' 'alac' 'als' 'ape' 'bonk' 'flac' 'kxs' 'la' 'lpac' 'mkw' 'ofr' 'shn' 'tak' 'tta' 'wav' 'wv'
|
|
# more common formats:
|
|
aFileExt=( 'aif' 'aiff' 'alac' 'als' 'ape' 'flac' 'lpac' 'ofr' 'shn' 'wav' 'wv' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=1
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Integ1}" )
|
|
aFileExt=( 'wav' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Acceptable file formats : ${sIntegWavFormat}"
|
|
"Acceptable sample rates, Hz : ${sEREIntegSampleRates}"
|
|
"Minimum audio length, sec. : ${iIntegMinLen}"
|
|
"Maximum audio length, sec. : ${iIntegMaxLen}"
|
|
"Minimum bit depth : ${iIntegMinBitdepth}"
|
|
"Maximum bit depth : ${iIntegMaxBitdepth}"
|
|
"Min. acceptable bit rate : ${iIntegMinBitrate}"
|
|
"Check junk at end : ${sIntegEndJunk}"
|
|
"Check inconsistent header : ${sIntegInconHeader}"
|
|
"Check canonical headers : ${sIntegCanHeaders}"
|
|
"Check extra RIFF chunks : ${sIntegExtRIFF}"
|
|
"Check odd data blk. padding : ${sIntegOddBlkPad}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Spectrogram}" )
|
|
aFileExt=( 'flac' 'wav' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=1
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Image size : ${sSpecFfmpegImgSz}"
|
|
"Viewer options : ${aSpecViewerExe[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_TrimSilence}" )
|
|
aFileExt=( 'flac' 'wav' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Encode}" )
|
|
aFileExt=( 'flac' 'wav' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Flac encoding options : ${aConvOptFlac[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_StripMeta}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Remove metadata : ${aCleanMetaflacOpt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_RestoreTags}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_FileNameToTags}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Tags to write : ${aFileToTagIDs[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
a=(
|
|
"File name RegEx : ${sFileNameToTagRE}"
|
|
)
|
|
printf '%s\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_WriteTag}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Comment tag options : ${aTagComment[*]}"
|
|
"Genre tag options : ${aTagGenre[*]}"
|
|
"Genre tag preview : ${sTagGenrePreview}"
|
|
"Music player options : ${aTagPlayExe}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Gain}" )
|
|
aFileExt=( 'flac' 'wav' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Rsgain options : ${aGainRsgainOpt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Integ2}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Required tags : ${aMetaReqTags[*]}"
|
|
"Max. file overhead : ${iMetaFileOverhead} bytes"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Optimize}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=1
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Metaflac options : ${aOptimizeMetaflacOpt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_FindDupes}" )
|
|
aFileExt=( 'flac' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"${sFmtInfo} This task will be run in batch mode ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_FileInfo}" )
|
|
aFileExt=( 'aif' 'aiff' 'alac' 'als' 'ape' 'flac' 'lpac' 'ofr' 'shn' 'wav' 'wv' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
|
|
( "${sTask_Play}" )
|
|
aFileExt=( 'n/a' )
|
|
iCalcFileSz=0
|
|
iDiskFreeChk=0
|
|
a=(
|
|
"${sFmtHeader} ${sTask} Settings ${sFmtEnd}"
|
|
"File types : ${aFileExt[*]}"
|
|
"Ffmpeg opt. : ${aPlayFfplay[*]}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
;;
|
|
( * )
|
|
# shellcheck disable=SC2016
|
|
printf '%s\n' 'ERROR @ "case "${sTask}" in" in f_taskInit()' ; exit 1
|
|
;;
|
|
esac
|
|
|
|
printf '%b\n' "${sFmtQuestion} Accept these settings? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
[[ "${REPLY}" = 'n' ]] && f_mainLoop
|
|
|
|
f_taskRun
|
|
}
|
|
|
|
f_taskRun () {
|
|
|
|
[[ "${iDev}" -eq 0 ]] && clear
|
|
|
|
declare -A aaTagPairs=()
|
|
declare -A aaFileNamePairs=()
|
|
|
|
local a=() aError=() aFiles=() aFileNames=() aFileHashes=() aFileSamples=() aFileSizes=() aDupHashes=() aDupNames=() aDupSizes=() \
|
|
i iBatchMode=0 iFileSzIn=0 iFileSzOut=0 iErrFiles=0 iGoodFiles=0 iNtcFiles=0 iTagFiles=0 iUsrFiles=0 iWrnFiles=0 \
|
|
sFileRenameRE='' sStatsFmt="${sFmtSuccess}"
|
|
|
|
# check for an unfinished 'working.*' file in /working
|
|
i="$( find -- './' -maxdepth '1' -type 'f' -iname 'working.*' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
a=(
|
|
"${sFmtWarning} There is a partially processed file named 'working.[ext]' in /working ${sFmtEnd}"
|
|
'This file must be renamed or removed before continuing'
|
|
"${sFmtQuestion} Press any key to return to the Main Menu ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
f_mainLoop
|
|
fi
|
|
|
|
# fill aFiles() array with file names in /working having extensions compatable with the chosen task
|
|
if [[ "${aFileExt[*]}" = 'n/a' ]] ; then
|
|
sRE='\.[^.]+$'
|
|
for sFile in * ; do
|
|
[[ "${sFile}" =~ ${sRE} ]] && aFiles+=( "${sFile}" )
|
|
done
|
|
else
|
|
for sExt in "${aFileExt[@]}" ; do
|
|
for sFile in *".${sExt}" ; do
|
|
[[ -f "${sFile}" ]] && aFiles+=( "${sFile}" )
|
|
done
|
|
done
|
|
fi
|
|
|
|
# offer options for processing the files - for some tasks we force batch processing
|
|
if [[ -z "${aFiles[*]}" ]] ; then
|
|
a=(
|
|
"${sFmtNotice} No files are compatible with the selected task ${sFmtEnd}"
|
|
"${sFmtQuestion} Press any key to return to the Main Menu ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
f_mainLoop
|
|
elif [[ "${sTask}" = "${sTask_EditFileName}" || "${sTask}" = "${sTask_FindDupes}" ]] ; then
|
|
iBatchMode=1
|
|
elif [[ "${#aFiles[@]}" -gt 1 && "${sTask}" != "${sTask_SplitAlbum}" ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} [P] process files singly, [B] batch process files, [S] select a file [S/b/f] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'b' ]] ; then
|
|
iBatchMode=1
|
|
elif [[ "${REPLY}" = 's' ]] ; then
|
|
select s in "${aFiles[@]}" 'Cancel'
|
|
do
|
|
[[ "${s}" = 'Cancel' ]] && f_mainLoop
|
|
aFiles=( "${s}" ) && break
|
|
done
|
|
fi
|
|
fi
|
|
|
|
if [[ "${sTask}" = "${sTask_FindDupes}" ]] ; then
|
|
if [[ "${#aFiles[@]}" -lt 2 ]] ; then
|
|
a=(
|
|
"${sFmtError} More than 1 file is needed to compare files ${sFmtEnd}"
|
|
"${sFmtQuestion} Press any key to return to the Main Menu ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
f_mainLoop
|
|
fi
|
|
|
|
# build arrays of data used to find duplicates
|
|
# array format: /file_name [hash|size|samples|fuzzy_name]/
|
|
# we're adding slashes as separators because later we'll be searching the
|
|
# arrays as a string which is *much* faster than looping through them
|
|
i=0
|
|
for iFile in "${!aFiles[@]}" ; do
|
|
# build araay of file CRC sums
|
|
sRet="$( cksum -- "${aFiles[iFile]}" )" # cksum returns: [CRC] [file_size] [file_name]
|
|
sRE='^([0-9]+)'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aFileHashes+=( "/${aFiles[iFile]} ${BASH_REMATCH[1]}/" )
|
|
|
|
# build aray of file sizes which cksum also provides
|
|
sRE='^[0-9]+ ([0-9]+)' # match file size
|
|
[[ "${sRet}" =~ ${sRE} ]] && aFileSizes+=( "/${aFiles[iFile]} ${BASH_REMATCH[1]}/" )
|
|
|
|
# build array of number of audio samples
|
|
sRet="$( metaflac --show-total-samples -- "${aFiles[iFile]}" )"
|
|
aFileSamples+=( "/${aFiles[iFile]} ${sRet}/" )
|
|
|
|
# build array of fuzzy file names
|
|
sRet="$( perl -fp -e 's/[[(].*[])]|[^[:alpha:]]//gi' <<< "${aFiles[iFile]}" )"
|
|
sRet="${sRet,,}" # lower case
|
|
aFileNames+=( "/${aFiles[iFile]} ${sRet}/" )
|
|
|
|
(( i += 1 ))
|
|
printf '%s\r' "Compiled information for ${i} files"
|
|
done
|
|
printf '\n'
|
|
fi
|
|
|
|
f_log "Running task: ${sTask}" '==='
|
|
|
|
[[ "${iDev}" -eq 0 ]] && clear
|
|
|
|
# reset timer so we can print process time at end of task if batch mode is active
|
|
SECONDS=0
|
|
|
|
# NOTE anytime a file name and/or extension is changed in a task we update aFiles()
|
|
# and/or $sFileExt within the task
|
|
# NOTE don't use 'iFile' as a variable in this loop
|
|
|
|
for iFile in "${!aFiles[@]}" ; do
|
|
aError=()
|
|
|
|
# remove non-print chars from file name
|
|
sRE='[^[:print:]]'
|
|
if [[ "${aFiles[iFile]}" =~ ${sRE} ]] ; then
|
|
aFiles[iFile]="${aFiles[iFile]//[^[:print:]]/}"
|
|
aError+=( '[INF] Non-printing characters were removed from the file name' )
|
|
fi
|
|
|
|
# these vars need to be set/reset after each file is processed
|
|
# $sLastFile is printed in main loop - it's repeated at the end of this loop
|
|
# where the file name could have changed by then, but it's here also in case
|
|
# we abort and return to the main loop early
|
|
sLastFile="${aFiles[iFile]}"
|
|
sFileOrigName="${aFiles[iFile]}" # store original file name so we can reference it later should it's name be changed by a task
|
|
sFileBaseName="${aFiles[iFile]%.*}"
|
|
sFileExt="${aFiles[iFile]##*.}"
|
|
iFileCount="$(( "${iFile}" + 1 ))"
|
|
# associative arrays must be emptied by re-declaring them else we can get 'unbound
|
|
# variable' error when we try to repopulate it
|
|
if [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
declare -A aaTagPairs=()
|
|
declare -A aaFileNamePairs=()
|
|
fi
|
|
|
|
if [[ "${sTask}" = "${sTask_FindDupes}" ]] ; then
|
|
printf '%s\r' "Processing (${iFileCount}/${#aFiles[@]}): ${aFiles[iFile]}"
|
|
else
|
|
s="Processing (${iFileCount}/${#aFiles[@]}): ${aFiles[iFile]}"
|
|
printf '%b\n' "${sFmtInfo} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
fi
|
|
|
|
# sanitize file name except for $sTask_Play where it's sanitized later
|
|
[[ "${sTask}" != "${sTask_Play}" ]] && mv -- "${aFiles[iFile]}" "working.${sFileExt}"
|
|
|
|
[[ "${iCalcFileSz}" -eq 1 ]] && iFileSzIn="$( du --summarize --bytes -- "working.${sFileExt}" | cut -f1 )"
|
|
|
|
case "${sTask}" in
|
|
( "${sTask_FileAttribs}" )
|
|
if chmod "${iChmodOpt}" -- "working.${sFileExt}" ; then
|
|
aError+=( "[INF] Set file permissions: ${iChmodOpt}" )
|
|
else
|
|
aError+=( '[NTC] Failed to set file permissions' )
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_SplitAlbum}" )
|
|
i=0
|
|
sFileCue="${sFileBaseName}.cue"
|
|
[[ ! -f "${sFileCue}" ]] && sFileCue="${sFileBaseName}.${sFileExt}.cue"
|
|
if [[ ! -f "${sFileCue}" ]] ; then
|
|
aError+=( '[NTC] No CUE file was found' )
|
|
else
|
|
# shntool exits non-zero if there's an error OR it doesn't need to do anything with the file
|
|
if ! shntool 'split' -f "${sFileCue}" -t "${aSplitShntoolOpt[@]}" -- "working.${sFileExt}" ; then
|
|
aError+=( '[ERR] Failed to process the file (shntool)' )
|
|
else
|
|
find -- './' -maxdepth '1' -type 'f' -iname '*pregap.wav' -delete
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_SaveTags}" )
|
|
if [[ -z "${aSaveTags[*]}" ]] ; then
|
|
s='Missing value for "aSaveTags" option'
|
|
aError+=( "[INF] ${s}" )
|
|
else
|
|
a=()
|
|
for i in "${!aSaveTags[@]}" ; do
|
|
if f_tagRead "${aSaveTags[${i}]}" ; then # returns aTagValues()
|
|
a+=( "${aSaveTags[${i}]}=${aTagValues[*]}" )
|
|
else
|
|
aError+=( "[TAG] Empty value for tag: ${aSaveTags[${i}]}" )
|
|
fi
|
|
done
|
|
|
|
# write the metadata file
|
|
if [[ "${#a[@]}" -gt 0 ]] ; then
|
|
printf '%s\n' "${a[@]}" > "../metadata/${sFileBaseName}.txt"
|
|
for s in "${a[@]}" ; do
|
|
aError+=( "[INF] Saved tag: ${s}" )
|
|
done
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_TagsToFileName}" )
|
|
s=''
|
|
if f_tagRead "${aTagToFileFormat[@]}" ; then # returns aTagValues()
|
|
s="${aTagValues[*]//[^[:print:]]/}" # remove non-print chars
|
|
sRE='^[ -]*$'
|
|
[[ "${aTagValues[*]}" =~ ${sRE} ]] && aError+=( '[TAG] Insufficient tag values' )
|
|
else
|
|
aError+=( '[TAG] Failed to read requested tags' )
|
|
fi
|
|
|
|
if [[ -z "${aError[*]}" ]] ; then
|
|
if [[ "${s}" != "${sFileBaseName}" ]] ; then
|
|
sRE='[[:graph:]] ?- ?[[:graph:]]'
|
|
if [[ "${s}" =~ ${sIllegalFileCharsRE} ]] ; then
|
|
aError+=( '[TAG] Proposed file name contains forbidden characters: ][*$|\/' )
|
|
elif [[ ! "${s}" =~ ${sRE} ]] ; then
|
|
aError+=( "[TAG] Proposed file name is badly formatted: '${s}'" )
|
|
elif [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
a=(
|
|
"${sFmtInfo} Old name: ${sFmtEnd} ${sFileBaseName}"
|
|
"${sFmtInfo} New name: ${sFmtEnd} ${s}"
|
|
"${sFmtQuestion} Accept the new file name? [Y/n] ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
[[ "${REPLY}" = 'n' ]] && aError+=( '[INF] Proposed file name was retained' )
|
|
fi
|
|
|
|
if [[ -z "${aError[*]}" ]] ; then
|
|
sFileBaseName="${s}"
|
|
# store new and original names so we can offer to restore old names later
|
|
[[ "${iBatchMode}" -eq 1 ]] && aaFileNamePairs+=( ["${sFileBaseName}.${sFileExt}"]="${aFiles[iFile]}" )
|
|
aFiles[iFile]="${sFileBaseName}.${sFileExt}"
|
|
aError+=( "[INF] File name changed to: ${sFileBaseName}.${sFileExt}" )
|
|
fi
|
|
else
|
|
aError+=( '[INF] Current and proposed file names are identical' )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_FormatFileName}" )
|
|
sNewBaseName="${sFileBaseName}"
|
|
|
|
# character conversion - this must be before user replacement expressions
|
|
if [[ "${sFmtFileNameIconv}" = 'enable' ]] ; then
|
|
if sRet="$( iconv --to-code='ASCII//TRANSLIT' <<< "${sNewBaseName}" )" ; then
|
|
sNewBaseName="${sRet}"
|
|
else
|
|
aError+=( '[ERR] Failed to process the file (iconv)' )
|
|
fi
|
|
fi
|
|
|
|
for s in "${aFmtFileNameRE[@]}" ; do
|
|
[[ -z "${s}" ]] && continue
|
|
if ! sNewBaseName="$( perl -fp -e "s/${s}/g" <<< "${sNewBaseName}")" ; then
|
|
aError+=( '[ERR] Failed to process the file (perl)' )
|
|
aError+=( "[INF] Check expression \"${s}\" in \"aFmtFileNameRE\" option" )
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "${aError[*]}" ]] ; then
|
|
sFileExt="${sFileExt,,}" # lower case extension
|
|
if [[ "${sNewBaseName}" != "${sFileBaseName}" ]] ; then
|
|
if [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
a=(
|
|
"${sFmtInfo} Old name: ${sFmtEnd} ${sFileBaseName}"
|
|
"${sFmtInfo} New name: ${sFmtEnd} ${sNewBaseName}"
|
|
"${sFmtQuestion} Accept the new name? [Y/n] ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
[[ "${REPLY}" = 'n' ]] && aError+=( '[INF] Proposed file name was rejected' )
|
|
fi
|
|
else
|
|
aError+=( '[INF] Current and proposed file names are identical' )
|
|
fi
|
|
|
|
# update file name in aFiles()
|
|
if [[ -z "${aError[*]}" ]] ; then
|
|
# store new and original names so we can offer to restore old names later
|
|
[[ "${iBatchMode}" -eq 1 ]] && aaFileNamePairs+=( ["${sNewBaseName}.${sFileExt}"]="${aFiles[iFile]}" )
|
|
aFiles[iFile]="${sNewBaseName}.${sFileExt}"
|
|
aError+=( "[INF] File name changed to: ${sNewBaseName}.${sFileExt}" )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_EditFileName}" )
|
|
sNewBaseName=''
|
|
# when running in batch mode, once $sFileRenameRE is populated we skip this 'if' statement
|
|
if [[ -z "${sFileRenameRE}" ]] ; then
|
|
a=(
|
|
"${sFmtQuestion} Enter a Perl Compatible Regular Expression and press [Enter] ${sFmtEnd}"
|
|
'* The match epression is case-sensitive and must end with a /'
|
|
'* Optionally, a replacement expression may follow the /'
|
|
'* Back-references are allowed in the format \\N'
|
|
'* Double quotes must be escaped ( \" )'
|
|
'See the README.md file for help'
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -re -p 'match/[replace]: '
|
|
sRE='[^[:print:]]'
|
|
if [[ -z "${REPLY}" || "${REPLY}" =~ ${sRE} ]] ; then
|
|
aError+=( '[PRN] Expression is empty or contains non-printing characters' )
|
|
iBatchMode=0
|
|
elif [[ "${REPLY}" != *'/'* ]] ; then
|
|
aError+=( '[PRN] Improperly formatted expression' )
|
|
iBatchMode=0
|
|
else
|
|
sFileRenameRE="${REPLY}"
|
|
fi
|
|
fi
|
|
|
|
# keep this separate from the above so it always runs
|
|
if [[ -n "${sFileRenameRE}" ]] ; then
|
|
if sRet="$( perl -fpe "s/${sFileRenameRE}/" <<< "${sFileBaseName}")" ; then
|
|
sNewBaseName="${sRet}"
|
|
else
|
|
printf '%s\n' "${sRet}"
|
|
aError+=( '[PRN] Syntax error in expression' )
|
|
iBatchMode=0
|
|
fi
|
|
# update file name in aFiles() - don't check aError() here since we need to
|
|
# continue even when a file produces an error
|
|
if [[ -n "${sNewBaseName}" ]] ; then
|
|
# store new and original names so we can offer to restore old names later
|
|
[[ "${iBatchMode}" -eq 1 ]] && aaFileNamePairs+=( ["${sNewBaseName}.${sFileExt}"]="${aFiles[iFile]}" )
|
|
aFiles[iFile]="${sNewBaseName}.${sFileExt}"
|
|
aError+=( "[INF] File name changed to: ${sNewBaseName}.${sFileExt}" )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Decompress}" )
|
|
f_convert 'decompress'
|
|
;;
|
|
|
|
( "${sTask_Integ1}" ) # wav only
|
|
# decode check - flac (and ffmpeg apparently) calcs a hash of the audio stream and
|
|
# compares it with the stored hash
|
|
if ! ffmpeg -hide_banner -loglevel 'warning' -xerror -err_detect '+crccheck+bitstream+buffer+explode' -vn -i "working.${sFileExt}" -f 'null' - ; then
|
|
aError+=( '[ERR] Failed to process the file (ffmpeg)' )
|
|
aError+=( '[WRN] File may be damaged, incompatible, or the audio may be altered' )
|
|
fi
|
|
|
|
if [[ -n "${aError[*]}" ]] ; then
|
|
aError+=( '[INF] Further testing was aborted' )
|
|
else
|
|
# file name ASCII check
|
|
if [[ "${sFmtFileNameAscii}" = 'enable' ]] && ! pcre2grep --quiet '^[ -~]+$' <<< "${sFileOrigName}" ; then
|
|
aError+=( '[NTC] File name contains non-ASCII characters' )
|
|
fi
|
|
|
|
# shntool info
|
|
# shntool exits non-zero if there's an error OR it doesn't need to do anything with the file
|
|
# for shntool message and return strings see:
|
|
# 'core_wave.c' and 'mode_info.c' in /.3rd party tools/shntool-3.0.10/src/
|
|
if ! sRet="$( shntool 'info' -- "working.${sFileExt}" )" ; then
|
|
printf '%s\n' "${sRet}"
|
|
aError+=( '[ERR] Failed to process the file (shntool)' )
|
|
else
|
|
# format check
|
|
sRE='WAVE format:[^(]*\(([^)]+)'
|
|
if [[ "${sRet}" =~ ${sRE} ]] ; then
|
|
s="${BASH_REMATCH[1]}"
|
|
[[ ! "${s}" =~ ${sIntegWavFormat} ]] && aError+=( "[WRN] Unacceptable WAVE format: ${s}" )
|
|
else
|
|
aError+=( '[WRN] Failed to determine WAVE format' )
|
|
fi
|
|
|
|
# compression check
|
|
sRE="File is compressed: *(yes|no)"
|
|
if [[ "${sRet}" =~ ${sRE} ]] ; then
|
|
[[ "${BASH_REMATCH[1]}" = 'yes' ]] && aError+=( '[WRN] PCM/WAV files should not be compressed' )
|
|
else
|
|
aError+=( '[WRN] Failed to determine if file is compressed' )
|
|
fi
|
|
|
|
# truncated check - flac files will often trigger this, so avoid testing them
|
|
sRE='File probably truncated: *yes'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[WRN] File appears to be truncated' )
|
|
|
|
# non-canonical header check
|
|
if [[ "${sIntegCanHeaders}" = 'enable' ]] ; then
|
|
sRE='Non-canonical header: *yes'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[NTC] File contains non-canonical header' )
|
|
fi
|
|
|
|
# inconsistent header check
|
|
if [[ "${sIntegInconHeader}" = 'enable' ]] ; then
|
|
sRE='Inconsistent header: *no'
|
|
[[ "${sRet}" =~ ${sRE} ]] || aError+=( '[WRN] File contains inconsistant header' )
|
|
fi
|
|
|
|
# junk check
|
|
if [[ "${sIntegEndJunk}" = 'enable' ]] ; then
|
|
sRE='Junk appended to file: *yes'
|
|
# we don't flag on 'unknown' since this is a very common result for flac
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[NTC] File has junk appended to the end' )
|
|
fi
|
|
|
|
# extra RIFF chunks check - this may be metadata
|
|
if [[ "${sIntegExtRIFF}" = 'enable' ]] ; then
|
|
sRE='Extra RIFF chunks: *yes'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[NTC] File contains extra RIFF chunks (could be metadata)' )
|
|
fi
|
|
|
|
# odd data block is padded check
|
|
if [[ "${sIntegOddBlkPad}" = 'enable' ]] ; then
|
|
sRE='Odd data size has pad byte: *no'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[NTC] Odd size data blocks are not padded' )
|
|
fi
|
|
fi
|
|
|
|
# for mediainfo, grab what we need from the Audio section
|
|
# use "mediainfo --Language='raw' -f <file>" to get the option text
|
|
a=()
|
|
for s in 'StreamCount' 'Format' 'Duration' 'BitRate_Mode' 'BitRate' 'Channel(s)' \
|
|
'SamplingRate' 'BitDepth'
|
|
do
|
|
sRet="$( mediainfo --Output="Audio;%${s}%" -- "working.${sFileExt}" )"
|
|
[[ -z "${sRet}" ]] && sRet='n/a'
|
|
a+=( "${sRet}" )
|
|
done
|
|
|
|
# StreamCount
|
|
if [[ "${a[0]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine number of audio streams' )
|
|
elif [[ "${a[0]}" -ne 1 ]] ; then
|
|
aError+=( "[WRN] Incorrect number of audio streams: ${a[0]}" )
|
|
fi
|
|
|
|
# Format
|
|
[[ "${a[1]}" = 'n/a' ]] && aError+=( '[WRN] Failed to determine format' )
|
|
|
|
# Duration
|
|
if [[ "${a[2]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine length of audio' )
|
|
else
|
|
a[2]="$(( "${a[2]}" / 1000 ))" # convert thousands to seconds
|
|
if [[ "${a[2]}" -lt "${iIntegMinLen}" ]] ; then
|
|
aError+=( "[WRN] Length of audio too short: ${a[2]} sec" )
|
|
elif [[ "${iIntegMaxLen}" -gt 0 && "${a[2]}" -gt "${iIntegMaxLen}" ]] ; then
|
|
aError+=( "[WRN] Length of audio too long: ${a[2]} sec" )
|
|
fi
|
|
fi
|
|
|
|
# BitRate_Mode
|
|
if [[ "${a[3]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine bit rate mode' )
|
|
elif [[ "${a[3]}" != 'CBR' ]] ; then
|
|
aError+=( "[WRN] Incorrect bit rate mode: ${a[3]}" )
|
|
fi
|
|
|
|
# BitRate
|
|
if [[ "${a[4]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine bit rate' )
|
|
elif [[ "${a[4]}" -lt "${iIntegMinBitrate}" ]] ; then
|
|
aError+=( "[WRN] Low bit rate: ${a[4]}" )
|
|
fi
|
|
|
|
# Channel(s)
|
|
if [[ "${a[5]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine number of audio channels' )
|
|
elif [[ "${a[5]}" != '2' ]] ; then
|
|
aError+=( "[WRN] Incorrect number of audio channels: ${a[5]} channel(s)" )
|
|
fi
|
|
|
|
# SamplingRate
|
|
if [[ "${a[6]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine sample rate' )
|
|
elif [[ ! "${a[6]}" =~ ${sEREIntegSampleRates} ]] ; then
|
|
aError+=( "[WRN] Unacceptable sample rate: ${a[6]} Hz" )
|
|
fi
|
|
|
|
# BitDepth
|
|
if [[ "${a[7]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine bit depth' )
|
|
elif [[ "${a[7]}" -lt "${iIntegMinBitdepth}" ]] ; then
|
|
aError+=( "[WRN] Low bit depth: ${a[7]}" )
|
|
elif [[ "${iIntegMaxBitdepth}" -gt 0 && "${a[7]}" -gt "${iIntegMaxBitdepth}" ]] ; then
|
|
aError+=( "[WRN] High bit depth: ${a[7]}" )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Integ2}" ) # flac only
|
|
if ! flac --silent --warnings-as-errors --test -- "working.${sFileExt}" ; then
|
|
printf '%s\n' "${sRet}"
|
|
aError+=( '[ERR] Failed to process the file (flac)' )
|
|
aError+=( '[WRN] File may be damaged, incompatible, or the audio may be altered' )
|
|
aError+=( '[INF] Further testing was aborted' )
|
|
elif ! sRet="$( shntool 'info' -- "working.${sFileExt}" )" ; then
|
|
printf '%s\n' "${sRet}"
|
|
aError+=( '[ERR] Failed to process the file (shntool)' )
|
|
aError+=( '[INF] Further testing was aborted' )
|
|
else
|
|
# ID3v2 tag check
|
|
sRE='File contains ID3v2 tag: *yes'
|
|
[[ "${sRet}" =~ ${sRE} ]] && aError+=( '[TAG] File contains ID3v2 tag' )
|
|
|
|
# for mediainfo, grab what we need from the Audio section
|
|
# use "mediainfo --Language='raw' -f <file>" to get the option text
|
|
a=()
|
|
for s in 'Format' 'Compression_Mode' 'ChannelPositions' 'StreamSize'
|
|
do
|
|
sRet="$( mediainfo --Output="Audio;%${s}%" -- "working.${sFileExt}" )"
|
|
[[ -z "${sRet}" ]] && sRet='n/a'
|
|
a+=( "${sRet}" )
|
|
done
|
|
|
|
# Format
|
|
[[ "${a[0]}" != 'FLAC' ]] && aError+=( "[WRN] Incorrect format: ${a[0]}" )
|
|
|
|
# Compression_Mode
|
|
if [[ "${a[1]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine compression mode' )
|
|
elif [[ "${a[1]}" != 'Lossless' ]] ; then
|
|
aError+=( "[WRN] Incorrect compression mode: ${a[1]}" )
|
|
fi
|
|
|
|
# ChannelPositions
|
|
if [[ "${a[2]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine channel layout' )
|
|
elif [[ "${a[2]}" != 'Front: L R' ]] ; then
|
|
aError+=( "[WRN] Incorrect channel layout: ${a[2]}" )
|
|
fi
|
|
|
|
# calc file overhead (uses StreamSize)
|
|
if [[ "${iMetaFileOverhead}" -gt 0 ]] ; then
|
|
if [[ "${a[3]}" = 'n/a' ]] ; then
|
|
aError+=( '[WRN] Failed to determine audio stream size' )
|
|
else
|
|
# subtract audio stream size from total file size and compare to user threshold
|
|
i="$( du --apparent-size --block-size='1' -- "working.${sFileExt}" | cut --fields='1' )"
|
|
i="$(( "${i}" - "${a[3]}" ))"
|
|
[[ "${i}" -gt "${iMetaFileOverhead}" ]] && aError+=( "[TAG] Excess file overhead: ${i} bytes" )
|
|
fi
|
|
fi
|
|
|
|
# check for user required tags
|
|
if [[ -n "${aMetaReqTags[*]}" ]] ; then
|
|
if f_tagRead "${aMetaReqTags[@]}" ; then
|
|
sRE='[[:graph:]]' # match any printable character except a space
|
|
for i in "${!aMetaReqTags[@]}" ; do
|
|
if [[ ! "${aTagValues[i]}" =~ ${sRE} ]] ; then
|
|
aError+=( "[TAG] Missing tag/value for tag: ${aMetaReqTags[i]}" )
|
|
fi
|
|
done
|
|
else
|
|
aError+=( '[TAG] One or more requested tags were not found' )
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Spectrogram}" )
|
|
if [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
# NOTE certain sizes, even some defined in ffmpeg manual (https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax) cause ffmpeg to hang
|
|
if ffmpeg -loglevel 'error' -hide_banner -xerror -y -i "working.${sFileExt}" -lavfi "showspectrumpic=size=${sSpecFfmpegImgSz}" -f 'image2' - | ffplay -loglevel 'error' -hide_banner -fs -window_title "${sFileOrigName}" - ; then
|
|
aError+=( '[INF] Generated temporary spectrogram for preview' )
|
|
else
|
|
aError+=( '[WRN] Failed to generate/display spectrogram (ffmpeg/ffplay)' )
|
|
fi
|
|
else
|
|
if ffmpeg -loglevel 'error' -hide_banner -xerror -y -i "working.${sFileExt}" -lavfi "showspectrumpic=size=${sSpecFfmpegImgSz}" -- "../spectro/${sFileOrigName}.png" > '/dev/null' ; then
|
|
aError+=( "[INF] Generated /spectro/${sFileOrigName}.png" )
|
|
else
|
|
aError+=( '[WRN] Failed to generate the spectrogram (ffmpeg)' )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_TrimSilence}" )
|
|
# shntool exits non-zero if there's an error OR it doesn't need to do anything with the file
|
|
shntool 'trim' -- "working.${sFileExt}"
|
|
if [[ -f 'working-trimmed.wav' ]] ; then
|
|
mv -- 'working-trimmed.wav' "working.${sFileExt}"
|
|
aError+=( '[INF] Trimmed silence' )
|
|
else
|
|
aError+=( '[INF] No excess silence detected' )
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Encode}" )
|
|
f_convert 'compress'
|
|
;;
|
|
|
|
( "${sTask_StripMeta}" )
|
|
if metaflac "${aCleanMetaflacOpt[@]}" -- "working.${sFileExt}" ; then
|
|
aError+=( '[INF] Stripped metadata' )
|
|
else
|
|
aError+=( '[ERR] Failed to process the file (metaflac)' )
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_RestoreTags}" )
|
|
if [[ ! -f "../metadata/${sFileBaseName}.txt" ]] ; then
|
|
aError+=( '[TAG] No tag file available in /metadata directory' )
|
|
else
|
|
# build associative array to feed to f_tagWrite()
|
|
while read -r s ; do
|
|
aError+=( "[INF] Tag to write: ${s}" )
|
|
aaTagPairs["${s%%=*}"]="${s#*=}"
|
|
done < "../metadata/${sFileBaseName}.txt"
|
|
|
|
if f_tagWrite "${aaTagPairs[@]}" ; then
|
|
aError+=( '[INF] Wrote tags:' "${aaTagPairs[@]}" )
|
|
else
|
|
aError+=( '[TAG] Failed to write tags' )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_FileNameToTags}" )
|
|
n=0
|
|
for i in "${!aFileToTagIDs[@]}" ; do
|
|
n="$(( i + 1 ))"
|
|
if s="$( pcre2grep --only-matching="${n}" "${sFileNameToTagRE}" <<< "${sFileBaseName}" )" ; then
|
|
aaTagPairs+=( ["${aFileToTagIDs[i]}"]="${s}" )
|
|
else
|
|
aError+=( '[TAG] Fewer file name segments than specified in "sFileNameToTagRE" option' ) ; break
|
|
fi
|
|
done
|
|
|
|
if [[ -n "${aaTagPairs[*]}" ]] ; then
|
|
if [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
a=( "${sFmtHeader} Tags to write: ${sFmtEnd}" )
|
|
# for printing, loop through aFileToTagIDs instead of aaTagPairs so we can print in the
|
|
# order specified by the former
|
|
for s in "${aFileToTagIDs[@]}" ; do
|
|
a+=( "${s} = ${aaTagPairs["${s}"]}" )
|
|
done
|
|
a+=( "${sFmtQuestion} Write these tags? [Y/n] ${sFmtEnd}" )
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
[[ "${REPLY}" = 'n' ]] && aaTagPairs=()
|
|
fi
|
|
|
|
# wtite tags from file name segments
|
|
if [[ -n "${aaTagPairs[*]}" ]] ; then
|
|
if f_tagWrite "${aaTagPairs[@]}" ; then
|
|
for i in "${!aaTagPairs[@]}" ; do
|
|
aError+=( "[INF] Wrote tag: ${i}=${aaTagPairs["${i}"]}" )
|
|
done
|
|
else
|
|
aError+=( '[TAG] Failed to write requested tags' )
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_WriteTag}" )
|
|
printf '%b\n' "${sFmtQuestion} Select a tag to write: ${sFmtEnd}"
|
|
|
|
# if batch mode is enabled then aaTagPairs() will be empty initially, so we only run
|
|
# the code in the 'else' statement once so the user can select a tag to write after
|
|
# which aaTagPairs() should be populated and so from then on we need to skip the
|
|
# 'else' code block
|
|
if [[ -n "${aaTagPairs[*]}" && "${iBatchMode}" -eq 1 ]] ; then
|
|
if f_tagWrite "${aaTagPairs[@]}" ; then
|
|
aError+=( "[INF] Wrote tag: ${sTagID}=${sTagValue}" )
|
|
else
|
|
aError+=( "[TAG] Failed to write tag: ${sTagID}" )
|
|
fi
|
|
else
|
|
sTagID='' # needs to be cleard only once before we enter the loop
|
|
# list of Vorbis tag IDs from https://www.xiph.org/vorbis/doc/v-comment.html
|
|
aTagID=( 'Title' 'Version' 'Album' 'Track' 'Artist' 'Performer' 'Copyright' 'License' 'Organization' 'Description' 'Genre' 'Date' 'Location' 'Contact' 'ISRC' 'Comment' 'Finish' 'Cancel' )
|
|
while true ; do
|
|
sTagValue='' # needs to be cleard each time before entering the select loop
|
|
select sSel in "${aTagID[@]}"
|
|
do
|
|
case "${sSel}" in
|
|
( 'Title' ) sTagID='TITLE' ;;
|
|
( 'Version' ) sTagID='VERSION' ;;
|
|
( 'Album' ) sTagID='ALBUM' ;;
|
|
( 'Track' ) sTagID='TRACKNUMBER' ;;
|
|
( 'Artist' ) sTagID='ARTIST' ;;
|
|
( 'Performer' ) sTagID='PERFORMER' ;;
|
|
( 'Copyright' ) sTagID='COPYRIGHT' ;;
|
|
( 'License' ) sTagID='LICENSE' ;;
|
|
( 'Organization' ) sTagID='ORGANIZATION' ;;
|
|
( 'Description' ) sTagID='DESCRIPTION' ;;
|
|
( 'Genre' )
|
|
sTagID='GENRE'
|
|
# preview the file if not in batch mode and user wants
|
|
[[ "${iBatchMode}" -eq 0 && "${sTagGenrePreview}" = 'enable' ]] && "${aTagPlayExe[@]}" "working.${sFileExt}" &
|
|
printf '%b\n' "${sFmtQuestion} Select a genre tag ${sFmtEnd}"
|
|
select s in "${aTagGenre[@]}" 'Custom' 'Cancel'
|
|
do
|
|
break
|
|
done
|
|
|
|
if [[ "${s}" = 'Custom' ]] ; then
|
|
sTagValue=''
|
|
elif [[ "${s}" = 'Cancel' ]] ; then
|
|
continue 2
|
|
else
|
|
sTagValue="${s}"
|
|
fi
|
|
;;
|
|
( 'Date' ) sTagID='DATE' ;;
|
|
( 'Location' ) sTagID='LOCATION' ;;
|
|
( 'Contact' ) sTagID='CONTACT' ;;
|
|
( 'ISRC' ) sTagID='ISRC' ;;
|
|
( 'Comment' )
|
|
sTagID='COMMENT'
|
|
if [[ -n "${aTagComment[*]}" ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Select a comment tag ${sFmtEnd}"
|
|
select s in "${aTagComment[@]}" 'Custom' 'Cancel'
|
|
do
|
|
break
|
|
done
|
|
|
|
if [[ "${s}" = 'Custom' ]] ; then
|
|
sTagValue=''
|
|
elif [[ "${s}" = 'Cancel' ]] ; then
|
|
break
|
|
else
|
|
# replace user variables
|
|
s="${s//'%v'/${sScriptVer}}"
|
|
s="${s//'%d'/$( date '+%m/%d/%Y' )}" # date: mo/dy/year
|
|
s="${s//'%t'/$( date '+%R' )}" # 24 hr time
|
|
sTagValue="${s}"
|
|
fi
|
|
else
|
|
s='Missing value for "aTagComment"'
|
|
printf '%b\n' "${sFmtWarning} ${s} ${sFmtEnd}"
|
|
aError+=( "[INF] ${s}" )
|
|
continue 2
|
|
fi
|
|
;;
|
|
( 'Finish'|'Cancel' ) break 2 ;;
|
|
( * ) printf '%b\n' "${sFmtWarning} Invalid choice ${sFmtEnd}" ;;
|
|
esac
|
|
|
|
if [[ -z "${sTagID}" ]] ; then
|
|
printf '%b\n' "${sFmtWarning} A tag ID was not specified ${sFmtEnd}"
|
|
break
|
|
elif [[ -z "${sTagValue}" ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Enter value for the ${sTagID} tag and press [Enter] ${sFmtEnd}"
|
|
read -re
|
|
sTagValue="${REPLY}"
|
|
fi
|
|
|
|
sTagValue="${sTagValue//[^[:print:]]/}" # remove non-printing chars
|
|
|
|
sRE='^[[:print:]]+$'
|
|
if [[ ! "${sTagValue}" =~ ${sRE} ]] ; then
|
|
printf '%b\n' "${sFmtWarning} An unacceptable or missing tag value was specified ${sFmtEnd}"
|
|
else
|
|
aaTagPairs=( ["${sTagID}"]="${sTagValue}" )
|
|
# this code should be similar to that before the start of this loop
|
|
if f_tagWrite "${aaTagPairs[@]}" ; then
|
|
s="Wrote tag: ${sTagID}=${sTagValue}"
|
|
aError+=( "[INF] ${s}" )
|
|
[[ "${iBatchMode}" -eq 0 ]] && printf '%b\n' "${sFmtInfo} ${s} ${sFmtEnd}"
|
|
else
|
|
aError+=( "[TAG] Failed to write tag: ${sTagID}" )
|
|
break 2
|
|
fi
|
|
fi
|
|
|
|
[[ "${iBatchMode}" -eq 1 ]] && break 2
|
|
break
|
|
done
|
|
done
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Gain}" )
|
|
sRE='--loudness=(-18)'
|
|
[[ "${aGainRsgainOpt[*]}" =~ ${sRE} ]]
|
|
if rsgain "${aGainRsgainOpt[@]}" -- "working.${sFileExt}" ; then
|
|
aError+=( "[INF] Added ReplayGain information at ${BASH_REMATCH[1]} LUFS" )
|
|
else
|
|
aError+=( '[ERR] Failed to process the file (rsgain)' )
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Optimize}" )
|
|
if [[ "${sFileExt}" = 'flac' && -n "${aOptimizeMetaflacOpt[*]}" ]] ; then
|
|
if ! sRet="$( metaflac "${aOptimizeMetaflacOpt[@]}" -- "working.${sFileExt}" )" ; then
|
|
printf '%s\n' "${sRet}"
|
|
aError+=( '[ERR] Failed to process the file (metaflac)' )
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_FindDupes}" )
|
|
# we're treating the arrays as strings as much as possible since looping through them is *painfully* slow
|
|
# array elements are formatted as: /file_name value/
|
|
# potentially duplicate file names are stored in aDup* arrays
|
|
|
|
sRE="/${aFiles[iFile]} ([^ ]+)/" # expression to grab the value we want from any of the arrays
|
|
for sAray in "${aFileHashes[*]}" "${aFileSizes[*]}" "${aFileSamples[*]}" "${aFileNames[*]}" ; do
|
|
[[ "${sAray}" =~ ${sRE} ]] && sValue="${BASH_REMATCH[1]}"
|
|
mapfile -t 'a' < <( pcre2grep --only-matching='1' "/(?!${aFiles[iFile]})([^/]+) ${sValue}/" <<< "${sAray}" )
|
|
if [[ "${#a[@]}" -gt 0 ]] ; then
|
|
for s in "${a[@]}" ; do
|
|
if [[ "${sAray}" = "${aFileHashes[*]}" ]] ; then
|
|
# avoid adding "a <=> b" to array if "b <=> a" already exists
|
|
if [[ "${aDupHashes[*]}" != "${s} <=> ${aFiles[iFile]}" ]] ; then
|
|
aDupHashes+=( "${aFiles[iFile]} <=> ${s}" )
|
|
fi
|
|
elif [[ "${sAray}" = "${aFileSizes[*]}" ]] ; then
|
|
if [[ "${aDupSizes[*]}" != "${s} <=> ${aFiles[iFile]}" ]] ; then
|
|
aDupSizes+=( "${aFiles[iFile]} <=> ${s}" )
|
|
fi
|
|
elif [[ "${sAray}" = "${aFileSamples[*]}" ]] ; then
|
|
if [[ "${aDupSamples[*]}" != "${s} <=> ${aFiles[iFile]}" ]] ; then
|
|
aDupSamples+=( "${aFiles[iFile]} <=> ${s}" )
|
|
fi
|
|
elif [[ "${sAray}" = "${aFileNames[*]}" ]] ; then
|
|
if [[ "${aDupNames[*]}" != *"${s} <=> ${aFiles[iFile]}"* ]] ; then
|
|
aDupNames+=( "${aFiles[iFile]} <=> ${s}" )
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
;;
|
|
|
|
( "${sTask_FileInfo}" )
|
|
a=(
|
|
"${sFmtHeader} FFPROBE INFORMATION ${sFmtEnd}"
|
|
'--------------------------------------------------------------------------------'
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
if ! ffprobe -hide_banner -loglevel 'info' -show_format -show_streams -show_error -print_format 'ini' -i "working.${sFileExt}" ; then
|
|
aError+=( '[ERR] Failed to process the file (ffprobe)' )
|
|
fi
|
|
|
|
printf '%b\n' "${sFmtHeader} SHNTOOL INFORMATION ${sFmtEnd}"
|
|
if ! shntool 'info' -- "working.${sFileExt}" ; then
|
|
aError+=( '[ERR] Failed to process the file (shntool)' )
|
|
fi
|
|
|
|
a=(
|
|
"${sFmtHeader} MEDIAINFO INFORMATION ${sFmtEnd}"
|
|
'--------------------------------------------------------------------------------'
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
# run mediainfo in sub-shell otherwise it outputs extra blank lines
|
|
if ! sRet="$( mediainfo -- "working.${sFileExt}" )" ; then
|
|
aError+=( '[ERR] Failed to process the file (mediainfo)' )
|
|
else
|
|
printf '%s\n' "${sRet}"
|
|
fi
|
|
;;
|
|
|
|
( "${sTask_Play}" )
|
|
mv -- "${aFiles[iFile]}" "working.${sFileExt}" # sanitize file name
|
|
a=( "${aPlayFfplay[@]/\'<file>\'/\'working.${sFileExt}\'}" )
|
|
ffplay -window_title "${sFileOrigName}" "${a[@]}" || aError+=( '[ERR] Failed to play file' )
|
|
;;
|
|
( * )
|
|
# shellcheck disable=SC2016
|
|
printf '%s\n' 'ERROR @ "case "${sTask}" in" in f_taskRun()' ; exit 1
|
|
;;
|
|
esac
|
|
|
|
# finish processing file
|
|
|
|
[[ "${iCalcFileSz}" -eq 1 ]] && iFileSzOut="$( du --summarize --bytes -- "working.${sFileExt}" | cut -f1 )"
|
|
|
|
# rename working file back to original name, or the new name if a task changed it we'll use
|
|
#--backup='numbered' option in case the new file name conflicts with and existing file
|
|
mv --backup='numbered' -- "working.${sFileExt}" "${aFiles[iFile]}"
|
|
|
|
[[ "${sTask}" != "${sTask_FindDupes}" ]] && printf '%b\n' "${sFmtHeader} ${sTask} Task Results ${sFmtEnd}"
|
|
|
|
if [[ -n "${aError[*]}" ]] ; then
|
|
f_log "${aError[@]}"
|
|
|
|
# format and print errors having a matching tag
|
|
# which get logged but not printed
|
|
# errors with [PRN] tag get printed but the files are not discarded
|
|
for s in "${aError[@]}" ; do
|
|
if [[ "${s}" = '[PRN] '* ]] ; then
|
|
s="${s/'[PRN] '/}" ; printf '%b%s\n' "${sFmtError} ERROR ${sFmtEnd}" " ${s}"
|
|
elif [[ "${s}" = '[ERR] '* ]] ; then
|
|
s="${s/'[ERR] '/}" ; printf '%b%s\n' "${sFmtError} ERROR ${sFmtEnd}" " ${s}"
|
|
elif [[ "${s}" = '[WRN] '* ]] ; then
|
|
s="${s/'[WRN] '/}" ; printf '%b%s\n' "${sFmtWarning} WARNING ${sFmtEnd}" " ${s}"
|
|
elif [[ "${s}" = '[NTC] '* ]] ; then
|
|
s="${s/'[NTC] '/}" ; printf '%b%s\n' "${sFmtNotice} NOTICE ${sFmtEnd}" " ${s}"
|
|
elif [[ "${s}" = '[TAG] '* ]] ; then
|
|
s="${s/'[TAG] '/}" ; printf '%b%s\n' "${sFmtNotice} METADATA ${sFmtEnd}" " ${s}"
|
|
elif [[ "${s}" = '[INF] '* ]] ; then
|
|
s="${s/'[INF] '/}" ; printf '%s\n' "${s}"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "${sTask}" != "${sTask_FindDupes}" ]] ; then
|
|
a=()
|
|
if [[ "${iCalcFileSz}" -eq 1 ]] ; then
|
|
a=(
|
|
"Starting file size: ${iFileSzIn} bytes"
|
|
"Finished file size: ${iFileSzOut} bytes"
|
|
)
|
|
fi
|
|
|
|
sRE='\[ERR\] |\[WRN\] |\[NTC\] |\[TAG\]'
|
|
if [[ ! "${aError[*]}" =~ ${sRE} ]] ; then
|
|
s='No anomalies detected'
|
|
f_log "${s}"
|
|
a+=( "${sFmtSuccess} ${s} ${sFmtEnd}" )
|
|
fi
|
|
|
|
a+=( "${sFmtInfo} Finished processing: ${aFiles[iFile]} ${sFmtEnd}" )
|
|
printf '%b\n' "${a[@]}"
|
|
fi
|
|
|
|
# count errors, set stats colors, move file to destination
|
|
sRE='\[(ERR|WRN|NTC|TAG)\] '
|
|
if [[ "${aError[*]}" =~ ${sRE} ]] ; then
|
|
# errors must be in decending order with the most critical at the top
|
|
if [[ "${aError[*]}" = *'[ERR] '* ]] ; then
|
|
(( iErrFiles += 1 )) ; sStatsFmt="${sFmtError}" ; sDir='../discard/unrepairable' ; s='File moved to /discard/unrepairable'
|
|
elif [[ "${aError[*]}" = *'[WRN] '* ]] ; then
|
|
(( iWrnFiles += 1 )) ; sStatsFmt="${sFmtWarning}" ; sDir='../discard/serious_issues' ; s='File moved to /discard/serious_issues'
|
|
elif [[ "${aError[*]}" = *'[NTC] '* ]] ; then
|
|
(( iNtcFiles += 1 )) ; sStatsFmt="${sFmtNotice}" ; sDir='../discard/minor_issues' ; s='File moved to /discard/minor_issues'
|
|
elif [[ "${aError[*]}" = *'[TAG] '* ]] ; then
|
|
(( iTagFiles += 1 )) ; sStatsFmt="${sFmtNotice}" ; sDir='../discard/metadata_issues' ; s='File moved to /discard/metadata_issues'
|
|
fi
|
|
|
|
# move file to destination except for specific tasks
|
|
if [[ "${iBatchMode}" -eq 1 && "${sAutoDiscard}" = 'enable' && "${sTask}" != "${sTask_SaveTags}" ]] ; then
|
|
mv --backup='numbered' -- "${aFiles[iFile]}" "${sDir}/${aFiles[iFile]}"
|
|
[[ "${sTask}" = "${sTask_SplitAlbum}" && -f "${sFileCue}" ]] && mv --backup='numbered' -- "${sFileCue}" "${sDir}/"
|
|
printf '%s\n' "${s}"
|
|
f_log "DISCARD: ${s}"
|
|
fi
|
|
else # good egg
|
|
(( iGoodFiles += 1 ))
|
|
fi
|
|
|
|
# printed in main loop - this also appears at the beginning of the this loop, however
|
|
# file name may have changed since then
|
|
sLastFile="${aFiles[iFile]}"
|
|
|
|
# always ask if user wants to keep the file if batch mode is not active
|
|
if [[ "${iBatchMode}" -eq 0 ]] ; then
|
|
if [[ "${sTask}" = "${sTask_SplitAlbum}" ]] ; then
|
|
a=( "${sFmtQuestion} Keep the album and CUE files? [Y/n] ${sFmtEnd}" )
|
|
else
|
|
a=( "${sFmtQuestion} Keep this file? [Y/n] ${sFmtEnd}" )
|
|
fi
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'n' ]] ; then
|
|
(( iUsrFiles += 1 ))
|
|
mv --backup='numbered' -- "${aFiles[iFile]}" "../discard/user_discard/"
|
|
if [[ "${sTask}" = "${sTask_SplitAlbum}" && -f "${sFileCue}" ]] ; then
|
|
mv --backup='numbered' -- "${sFileCue}" '../discard/user_discard/'
|
|
fi
|
|
s='File moved to /discard/user_discard'
|
|
printf '%s\n' "${s}"
|
|
f_log "DISCARD: ${s}"
|
|
elif [[ "${sTask}" = "${sTask_SplitAlbum}" ]] ; then
|
|
|
|
mv --backup='numbered' -- "${aFiles[iFile]}" '../backup/'
|
|
[[ -f "${sFileCue}" ]] && mv --backup='numbered' -- "${sFileCue}" '../backup/'
|
|
fi
|
|
|
|
# ask whether to continue with next file except for specific tasks
|
|
if [[ "${iFile}" -lt "${#aFiles[@]}-1" ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Continue with the next file? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
[[ "${REPLY}" = 'n' ]] && break
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# offer to undo file name changes if any were made - this won't work for backup~ files which is probably acceptable
|
|
# the keys in aaFileNamePairs() are the new names and the vlaues are the old names
|
|
if [[ "${iBatchMode}" -eq 1 && -n "${aaFileNamePairs[*]}" ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Press [Y] to accept the changes or [N] to undo them [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'n' ]] ; then
|
|
i="$( find -- './' -maxdepth '1' -type 'f' -iname '*.*' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
for i in "${!aaFileNamePairs[@]}" ; do
|
|
[[ -f "${i}" ]] && mv --backup='numbered' -- "${i}" "${aaFileNamePairs[${i}]}"
|
|
done
|
|
f_log 'Original file names were restored'
|
|
else
|
|
a=(
|
|
"${sFmtError} There are no files in /working ${sFmtEnd}"
|
|
"${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
fi
|
|
fi
|
|
elif [[ "${sTask}" = "${sTask_FindDupes}" ]] ; then
|
|
# we need the leading \n in printf because we overwrote the "Processing ... " line earlier
|
|
printf '\n%b\n' "${sFmtHeader} ${sTask} Task Results ${sFmtEnd}"
|
|
|
|
sDiv='--------------------------------------------------------------------------------'
|
|
|
|
# write duplicates.txt (same hashes)
|
|
if [[ -n "${aDupHashes[*]}" ]] ; then
|
|
printf '%s\n' >> '../duplicates.txt' '' 'FILES WITH IDENTICAL CRC CHECKSUMS' "${sDiv}" "${aDupHashes[@]}"
|
|
s='Files with identical CRC checksums'
|
|
printf '%b\n' "${sFmtNotice} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
else
|
|
s='No duplicates found based on CRC checksums'
|
|
printf '%b\n' "${sFmtSuccess} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
fi
|
|
|
|
# write duplicates.txt (exact file size matches)
|
|
if [[ -n "${aDupSizes[*]}" ]] ; then
|
|
printf '%s\n' >> '../duplicates.txt' '' 'FILES WITH IDENTICAL FILE SIZES' "${sDiv}" "${aDupSizes[@]}"
|
|
s='Files with identical file sizes'
|
|
printf '%b\n' "${sFmtNotice} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
else
|
|
s='No duplicates found based on file size'
|
|
printf '%b\n' "${sFmtSuccess} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
fi
|
|
|
|
# write duplicates.txt (same number of audio samples)
|
|
if [[ -n "${aDupSamples[*]}" ]] ; then
|
|
printf '%s\n' >> '../duplicates.txt' '' 'FILES WITH IDENTICAL NUMBER OF AUDIO SAMPLES' "${sDiv}" "${aDupSamples[@]}"
|
|
s='Files with identical number of audio samples'
|
|
printf '%b\n' "${sFmtNotice} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
else
|
|
s='No duplicates found based on number of audio samples'
|
|
printf '%b\n' "${sFmtSuccess} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
fi
|
|
|
|
# write duplicates.txt (fuzzy file name matches)
|
|
if [[ -n "${aDupNames[*]}" ]] ; then
|
|
printf '%s\n' >> '../duplicates.txt' '' 'FILES WITH SIMILAR FILE NAMES' "${sDiv}" "${aDupNames[@]}"
|
|
s='Files with similar file names'
|
|
printf '%b\n' "${sFmtNotice} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
else
|
|
s='No duplicates found based on file names'
|
|
printf '%b\n' "${sFmtSuccess} ${s} ${sFmtEnd}"
|
|
f_log "${s}"
|
|
fi
|
|
|
|
if [[ -n "${aDupHashes[*]}" || -n "${aDupSizes[*]}" || -n "${aDupSamples[*]}" || -n "${aDupNames[*]}" ]] ; then
|
|
printf '%s\n' 'Potentially duplicate files written to duplicates.txt'
|
|
fi
|
|
elif [[ "${iBatchMode}" -eq 1 && "${sTask}" = "${sTask_Spectrogram}" && -n "${aSpecViewerExe[*]}" ]] ; then
|
|
i="$( find -- '../spectro/' -maxdepth '1' -type 'f' -iname '*.png' | wc -l )" # count objects
|
|
[[ "${i}" -gt 0 ]] && "${aSpecViewerExe[@]}" &> '/dev/null' &
|
|
fi
|
|
|
|
# format, color and print stats
|
|
s1=" 0" && [[ "${iGoodFiles}" -gt 0 ]] && s1="${sFmtSuccess} ${iGoodFiles} ${sFmtEnd}"
|
|
s2=" 0" && [[ "${iTagFiles}" -gt 0 ]] && s2="${sFmtNotice} ${iTagFiles} ${sFmtEnd}"
|
|
s3=" 0" && [[ "${iNtcFiles}" -gt 0 ]] && s3="${sFmtNotice} ${iNtcFiles} ${sFmtEnd}"
|
|
s4=" 0" && [[ "${iWrnFiles}" -gt 0 ]] && s4="${sFmtWarning} ${iWrnFiles} ${sFmtEnd}"
|
|
s5=" 0" && [[ "${iErrFiles}" -gt 0 ]] && s5="${sFmtError} ${iErrFiles} ${sFmtEnd}"
|
|
s6=" 0" && [[ "${iUsrFiles}" -gt 0 ]] && s6="${sFmtNotice} ${iUsrFiles} ${sFmtEnd}"
|
|
i="$(( iErrFiles + iWrnFiles + iNtcFiles + iTagFiles ))"
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
sErrFiles="${sStatsFmt} ${i} ${sFmtEnd}"
|
|
else
|
|
sErrFiles="${sFmtSuccess} ${i} ${sFmtEnd}"
|
|
fi
|
|
|
|
# if the number of files processed != the total number of files, let's highlight the difference in the stats
|
|
i="$(( iGoodFiles + iTagFiles + iNtcFiles + iWrnFiles + iErrFiles + iUsrFiles ))"
|
|
|
|
# this is so we can subtract from $#aFiles[@] - there may be a way of subtracting
|
|
# 1 from the array index numner instead of doping this
|
|
iFiles="${#aFiles[@]}"
|
|
|
|
# if we split an album, the cue file is not included in aFiles(), so we need to add
|
|
# 1 to iFiles so we don't get an incorrect skipped file count
|
|
[[ "${sTask}" = "${sTask_SplitAlbum}" ]] && (( iFiles += 1 ))
|
|
|
|
if [[ "${i}" -ne "${iFiles}" ]] ; then
|
|
sSkippedFiles="${sFmtNotice} $(( iFiles - i )) ${sFmtEnd}"
|
|
else
|
|
sSkippedFiles="${sFmtSuccess} 0 ${sFmtEnd}"
|
|
fi
|
|
|
|
printf '%b\n' "${sFmtHeader} ${sTask} Task Statistics ${sFmtEnd}"
|
|
a=(
|
|
"Metadata issues :${s2}"
|
|
"Minor issues :${s3}"
|
|
"Serious issues :${s4}"
|
|
"Unrepairable :${s5}"
|
|
"User discard :${s6}"
|
|
"${sFmtHeader} Totals ${sFmtEnd}"
|
|
"Good files :${s1}"
|
|
"Problem files :${sErrFiles}"
|
|
"Skipped files :${sSkippedFiles}"
|
|
"Total files : ${#aFiles[@]}"
|
|
)
|
|
|
|
if [[ "${iBatchMode}" -eq 1 ]] ; then
|
|
iElapTime="${SECONDS}"
|
|
iElapTime="$( f_time "${iElapTime}" )"
|
|
a+=( "Time Elapsed : ${iElapTime}" )
|
|
if [[ "${sNotify}" = 'enable' ]] ; then
|
|
notify-send -a "${sScriptName}" "${sTask} operation complete." "Time Elapsed: ${iElapTime}"
|
|
fi
|
|
fi
|
|
printf '%b\n' "${a[@]}"
|
|
|
|
# completed tasks are displayed in the main loop and written to log file so we
|
|
# print them in the main loop if a session is continued
|
|
if [[ -n "${sCompletedTasks}" ]] ; then
|
|
sCompletedTasks+=" > ${sTask}"
|
|
else
|
|
sCompletedTasks+="${sTask}"
|
|
fi
|
|
|
|
# remove color codes for log file
|
|
for i in "${!a[@]}" ; do
|
|
a[i]="$( perl -pe 's/\\e\S+ | \\e\[0m/ /g' <<< "${a[i]}" )"
|
|
done
|
|
f_log '' "${sTask} Task Results" '---' "${a[@]}" '' "Completed tasks: ${sCompletedTasks}" "Last file: ${sLastFile}" '---' ''
|
|
|
|
printf '%b\n' "${sFmtQuestion} Press any key to return to the Main Menu ${sFmtEnd}"
|
|
read -rsN1
|
|
|
|
f_mainLoop
|
|
}
|
|
|
|
f_convert () { # $1=compress|decompress; returns 1 on error
|
|
|
|
case "${1}" in
|
|
( 'decompress' )
|
|
|
|
# shntool exits non-zero if there's an error OR it doesn't need to do anything with the file - for example...
|
|
# exits 1 = file exists, file not CD quality, nothing needed done
|
|
# shntool doesn't allow to set the full file name so we append '-out'
|
|
shntool 'conv' -a 'out_' -- "working.${sFileExt}"
|
|
if [[ -f 'out_working.wav' ]] ; then
|
|
rm -I -- "working.${sFileExt}"
|
|
mv -- 'out_working.wav' 'working.wav'
|
|
sFileExt='wav'
|
|
aFiles[iFile]="${sFileBaseName}.wav"
|
|
aError+=( "[INF] File name changed to: ${sFileBaseName}.wav" )
|
|
elif [[ -f "out_working.${sFileExt}.wav" ]] ; then
|
|
rm -I -- "working.${sFileExt}"
|
|
mv -- "out_working.${sFileExt}.wav" 'working.wav'
|
|
aFiles[iFile]="${sFileBaseName}.wav"
|
|
aError+=( "[INF] File name changed to: ${sFileBaseName}.wav" )
|
|
aError+=( "[NTC] The source file was not a ${sFileExt} file" )
|
|
sFileExt='wav' # needs to be after aError() above
|
|
else
|
|
aError+=( '[ERR] Failed to process the file (shntool)' ) ; return 1
|
|
fi
|
|
;;
|
|
|
|
( 'compress' )
|
|
# we'll give the output file a different name in case user is encoding flac to flac
|
|
if ! flac "${aConvOptFlac[@]}" --warnings-as-errors --output-name='out_working.flac' -- "working.${sFileExt}" ; then
|
|
aError+=( '[ERR] Failed to process the file (flac)' ) ; return 1
|
|
fi
|
|
|
|
# change working file name
|
|
if [[ -f 'out_working.flac' ]] ; then
|
|
rm -- "working.${sFileExt}" # let's do the deleting rather than having flac do it
|
|
mv -- 'out_working.flac' 'working.flac'
|
|
sFileExt='flac'
|
|
aFiles[iFile]="${sFileBaseName}.${sFileExt}"
|
|
aError+=( "[INF] File name changed to: ${sFileBaseName}.${sFileExt}" )
|
|
else
|
|
aError+=( '[ERR] There was an error encoding the file' ) ; return 1
|
|
fi
|
|
;;
|
|
|
|
( * )
|
|
# shellcheck disable=SC2016
|
|
printf '%s\n' 'ERROR @ "case "${1} in" in f_convert()' ; exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
f_tagRead () { # $1=array of tag names to read; sets array aTagValues(); returns 1 on error
|
|
|
|
# we have to copy the array sent to this func in order to operate on it
|
|
local a=( "$@" ) s sRet
|
|
|
|
# we run ffprobe in a loop because we can't depend on it to return values in the order requested
|
|
aTagValues=() # we must clear this array
|
|
for s in "${a[@]}" ; do
|
|
if [[ "${s}" = ' - ' ]] ; then # we got sent a separator from tag to file task
|
|
aTagValues+=( '-' ) # i don't know this works and ' - ' doesn't, but the latter changes file name to 'a - b'
|
|
elif sRet="$( ffprobe -loglevel 'error' -show_entries "format_tags=${s}" -of 'default=noprint_wrappers=1:nokey=1' -i "working.${sFileExt}" )" ; then
|
|
aTagValues+=( "${sRet}" )
|
|
else
|
|
return 1
|
|
fi
|
|
done
|
|
[[ -n "${aTagValues[*]}" ]] || return 1
|
|
}
|
|
|
|
f_tagWrite () { # $1=associative array of tag ID's and values; returns 1 on error
|
|
|
|
local a=() i
|
|
|
|
[[ -z "${aaTagPairs[*]}" ]] && return 1
|
|
|
|
for i in "${!aaTagPairs[@]}" ; do
|
|
a+=( "--remove-tag=${i}" )
|
|
# check for empty value for tag ID since we may only be deleting tags
|
|
if [[ -n "${aaTagPairs["${i}"]}" ]] ; then
|
|
a+=( "--set-tag=${i}=${aaTagPairs["${i}"]}" )
|
|
fi
|
|
done
|
|
metaflac "${a[@]}" -- "working.${sFileExt}" || return 1
|
|
}
|
|
|
|
f_time () {
|
|
|
|
local iH iM iS
|
|
|
|
(( iH="${1}" / 3600 ))
|
|
(( iM=( "${1}" % 3600 ) / 60 ))
|
|
(( iS="${1}" % 60 ))
|
|
printf '%02d:%02d:%02d\n' "${iH}" "${iM}" "${iS}"
|
|
}
|
|
|
|
# because we're feeding 'source' a variable and shellcheck hates us for it,
|
|
# we have to declare here all vars in the configuration file, however this
|
|
# also provides default values if no config file exists
|
|
f_defaultSettings () {
|
|
sScriptUpdate='enable'
|
|
sAutoDiscard='enable'
|
|
sNotify='enable'
|
|
iDiskFree=1000
|
|
iChmodOpt=644
|
|
aSplitShntoolOpt=( '%p - %t' )
|
|
sIntegWavFormat='Microsoft PCM|WAVE Extensible format'
|
|
sEREIntegSampleRates='44100|48000|88200|96000'
|
|
iIntegMinBitdepth=16
|
|
iIntegMaxBitdepth=24
|
|
iIntegMinBitrate=1411000
|
|
iIntegMinLen=60
|
|
iIntegMaxLen=900
|
|
sIntegInconHeader='enable'
|
|
sIntegCanHeaders='enable'
|
|
sIntegExtRIFF='enable'
|
|
sIntegEndJunk='enable'
|
|
sIntegOddBlkPad='enable'
|
|
aMetaReqTags=( 'ARTIST' 'TITLE' 'GENRE' 'COMMENT' 'REPLAYGAIN_TRACK_GAIN' 'REPLAYGAIN_TRACK_PEAK' )
|
|
iMetaFileOverhead=2048
|
|
aCleanMetaflacOpt=( '--remove-all' )
|
|
sSpecFfmpegImgSz='1024x512'
|
|
aSpecViewerExe=( 'gwenview' '--fullscreen' '../spectro/' )
|
|
aConvOptFlac=( '--verify' '--compression-level-5' )
|
|
aSaveTags=( 'ARTIST' 'TITLE' 'GENRE' )
|
|
aTagToFileFormat=( 'ARTIST' ' - ' 'TITLE' )
|
|
aFileToTagIDs=( 'ARTIST' 'TITLE' )
|
|
sFileNameToTagRE='^(.+?) - (.+)'
|
|
aTagGenre=( 'Ambient' 'Soft' 'Medium' 'Hard' )
|
|
sTagGenrePreview='enable'
|
|
aTagComment=( 'Muzik Faktry v%v' )
|
|
aTagPlayExe=( 'ffplay' '-loglevel' 'error' '-nostats' '-autoexit' '-showmode' 'rdft' )
|
|
aGainRsgainOpt=( 'custom' '--quiet' '--loudness=-18' '--tagmode=i' '--clip-mode=p' )
|
|
aOptimizeMetaflacOpt=( '--remove' '--block-type=PADDING' '--dont-use-padding' )
|
|
sFmtFileNameIconv='enable'
|
|
aFmtFileNameRE=(
|
|
'_/ ' # 1
|
|
'[[<]/(' # 2
|
|
'[]>]/)' # 3
|
|
'"/' # 4
|
|
"[<>]/'" # 5
|
|
'\s{2,}/ ' # 6
|
|
'(?i)^\s?(?:\(?[0-9-]{2,}\)?)(?!\S| special| sound system)[-. ]*/' # 7
|
|
'(?i)^(the)\s(\S.+?)(\s-\s)/\2, \1\3' # 8
|
|
'\s+$/' # 9
|
|
'(?i)((?:^|\s|\.|-)[a-z])([a-z]*)/\U\1\L\2' # 10
|
|
'([ (])(?:ft|Ft|Feat)\. /\1feat. ' # 11
|
|
'(?i)([ (])pts?\. /\1part ' # 12
|
|
'\(Live([ )])/(live\1' # 13
|
|
'(?i)^(abc|abba|atc|kc|reo|tlc|zz)[ .]/\U\1 ' # 14
|
|
'(?i)dna remix/DNA Remix' # 15
|
|
'Macgregor/MacGregor' # 16
|
|
'Mclachlan/McLachlan' # 17
|
|
'P!nk/Pink' # 18
|
|
)
|
|
sFmtFileNameAscii='enable'
|
|
aPlayFfplay=( '-loglevel' 'error' '-hide_banner' '-fs' '-f' 'lavfi' "amovie='<file>',asplit[a][out1];[a]showfreqs=mode=line:fscale=log[out0]" )
|
|
}
|
|
|
|
f_log () { # $1=array/strings to print, OR one of: openSession|resumeSession|pauseSession|closeSession
|
|
|
|
local a=( "$@" ) s
|
|
|
|
# it's much easier do these replacements separately
|
|
for i in "${!a[@]}" ; do
|
|
[[ "${a[i]}" = '---' ]] && a[i]='--------------------------------------------------------------------------------' && continue
|
|
[[ "${a[i]}" = '===' ]] && a[i]='================================================================================'
|
|
done
|
|
|
|
for s in "${a[@]}" ; do
|
|
case ${s} in
|
|
( 'openSession' )
|
|
a=(
|
|
'' '################################################################################'
|
|
"${sScriptName} v${sScriptVer}"
|
|
"SESSION OPENED: $( date '+%c' )"
|
|
"Configuration loaded: ${sConfig}"
|
|
'################################################################################' ''
|
|
)
|
|
;;
|
|
( 'resumeSession' )
|
|
a=(
|
|
'' "${sScriptName} v${sScriptVer}"
|
|
"SESSION RESUMED: $( date '+%c' )"
|
|
"Configuration loaded: ${sConfig}"
|
|
'================================================================================' ''
|
|
)
|
|
;;
|
|
( 'pauseSession' )
|
|
a=(
|
|
'' "SESSION PAUSED: $( date '+%c' )"
|
|
'================================================================================' ''
|
|
)
|
|
;;
|
|
( 'closeSession' )
|
|
a=(
|
|
'================================================================================'
|
|
"SESSION CLOSED: $( date '+%c' )" ''
|
|
)
|
|
;;
|
|
( * ) true ;;
|
|
esac
|
|
done
|
|
|
|
printf '%s\n' "${a[@]}" >> '../logs/session.log'
|
|
}
|
|
|
|
f_houseKeeping () { # $1=[openSession[closeSession]]
|
|
|
|
local i s
|
|
|
|
if [[ "${1}" = 'openSession' && ! -f '../logs/session.log' ]] ; then
|
|
if touch '../logs/session.log' ; then
|
|
f_log 'openSession'
|
|
else
|
|
printf '%b\n' "${sFmtError} Failed to create file: '/logs/session.log' ${sFmtEnd}" ; exit 1
|
|
fi
|
|
elif [[ "${1}" = 'openSession' ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Resume the last session? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" != 'n' ]] ; then
|
|
s="$( pcre2grep --only-matching='1' --multiline '(?:.|\n)*Completed tasks: (.+)' '../logs/session.log' )"
|
|
[[ -n "${s}" ]] && sCompletedTasks="${s}"
|
|
s="$( pcre2grep --only-matching='1' --multiline '(?:.|\n)*Last file: (.+)' '../logs/session.log' )"
|
|
[[ -n "${s}" ]] && sLastFile="${s}"
|
|
f_log 'resumeSession'
|
|
else
|
|
f_log 'closeSession'
|
|
s="$( date '+%F %R:%S' )" # 24 hr time so files can be properly sorted in file manager
|
|
mv -- '../logs/session.log' "../logs/session_${s}.log"
|
|
f_log 'openSession'
|
|
fi
|
|
else # 'closeSession'
|
|
printf '%b\n' "${sFmtQuestion} Close out log file for this session? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" != 'n' ]] ; then
|
|
f_log 'closeSession'
|
|
s="$( date '+%F %R:%S' )" # 24 hr time so files can be properly sorted in file manager
|
|
mv -- '../logs/session.log' "../logs/session_${s}.log"
|
|
else
|
|
f_log 'pauseSession'
|
|
fi
|
|
fi
|
|
|
|
# clean up /logs direcctory
|
|
if [[ "${1}" = 'openSession' ]] ; then
|
|
i="$( find -- '../logs/' -maxdepth '1' -type 'f' -iname '*.log' -not -iname 'session.log' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Delete ${i} old log files? [y/N] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'y' ]] ; then
|
|
find -- '../logs/' -maxdepth '1' -type 'f' -iname '*.log' -not -iname 'session.log' -delete
|
|
printf '%s\n' 'Deleted old log files'
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# clean up directories
|
|
for sDir in '../spectro/' '../metadata/' '../discard/metadata_issues/' '../discard/minor_issues' '../discard/serious_issues' '../discard/unrepairable' '../discard/user_discard' ; do
|
|
i="$( find -- "${sDir}" -maxdepth '1' -type 'f' -iname '*.*' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Delete ${i} files in ${sDir} directory? [y/N] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'y' ]] ; then
|
|
find -- "${sDir}" -maxdepth '1' -type 'f' -iname '*.*' -delete
|
|
printf '%s\n' "Deleted ${i} files in ${sDir}"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# empty duplicates.txt
|
|
i="$( du -- '../duplicates.txt' | cut -f1 )"
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Empty duplicates.txt file? [y/N] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" = 'y' ]] ; then
|
|
true > '../duplicates.txt'
|
|
s='Emptied duplicates.txt file'
|
|
printf '%s\n' "${s}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
f_mainLoop () {
|
|
|
|
local a=() aFiles=() i iDU=0 iDupes=0 iFiles=0 iTtlDU=0 iTtlFiles=0 s sFileExt sRE sSel sTask
|
|
|
|
[[ "${iDev}" -eq 0 ]] && clear
|
|
|
|
printf '%b\n' "${sFmtMenu} Main Menu :: Tasks ${sFmtEnd}"
|
|
|
|
# warn on low disk space
|
|
if [[ "${iDiskFree}" -gt 0 && "${iDiskFreeChk}" -eq 1 ]] ; then
|
|
s="$( df --block-size='M' --output='source,avail' '.' )"
|
|
sRE='/dev/[^ ]+ *([0-9]+)'
|
|
[[ "${s}" =~ ${sRE} ]]
|
|
if [[ "${BASH_REMATCH[1]}" -lt "${iDiskFree}" ]] ; then
|
|
printf '%b\n' "${sFmtWarning} Low disk space: ${BASH_REMATCH[1]} MB remaining ${sFmtEnd}"
|
|
fi
|
|
fi
|
|
|
|
# print completed tasks, last processed file
|
|
[[ -n "${sCompletedTasks}" ]] && printf '%b\n' "${sFmtInfo} Completed tasks: ${sFmtEnd} ${sCompletedTasks}"
|
|
[[ -n "${sLastFile}" ]] && printf '%b\n' "${sFmtInfo} Last file: ${sFmtEnd} ${sLastFile}"
|
|
|
|
# print stats for selected directories
|
|
printf '%b%-13s%-10s%-10s%-10s%-10b\n' "${sFmtHeader}" ' Directory' 'Files' 'Size' 'Type' "${sFmtEnd}"
|
|
for sDir in 'backup' 'holding' 'discard' 'finished' 'metadata' 'spectro' 'working' ; do
|
|
# need to reset these each time
|
|
iFiles=0
|
|
sFileExt=''
|
|
iDU=0
|
|
|
|
mapfile -d '' 'aFiles' < <( find -- "../${sDir}/" -maxdepth '1' -type 'f' -iname '*.*' -print0 )
|
|
if [[ -n ${aFiles[*]} ]] ; then
|
|
sRE='\.([^.]+)$' # match file extension
|
|
for s in "${aFiles[@]}" ; do
|
|
(( iFiles += 1 ))
|
|
(( iTtlFiles += 1 ))
|
|
[[ "${s}" =~ ${sRE} ]] && sFileExt+="${BASH_REMATCH[1]} "
|
|
[[ "${sDir}" = 'working' && "${s}" = *'~' ]] && (( iDupes += 1 )) # count duplicate files in /working
|
|
done
|
|
# sort the file extensions and dump all but unique
|
|
sFileExt="$( printf '%s' "${sFileExt}" | tr ' ' '\n' | sort --ignore-case --unique | tr '\n' ' ' )"
|
|
|
|
# get diectory sizes
|
|
iDU="$( du --summarize --bytes -- "../${sDir}/" | cut -f1 )" # bytes
|
|
iDU="$( bc <<< "scale=2 ; ${iDU} / 1000000" )" # convert to MB^10 (N.NN)
|
|
iTtlDU="$( bc <<< "scale=2 ; ${iTtlDU} + ${iDU}" )"
|
|
fi
|
|
|
|
# build print string:
|
|
printf '%-13s%-10d%-10s%-10s\n' " ${sDir}" "${iFiles}" "${iDU}" "${sFileExt}"
|
|
done
|
|
printf '%b%-13s%-10d%-10s%-10b\n' "${sFmtHeader}" ' TOTALS:' "${iTtlFiles}" "${iTtlDU} MB" " ${sFmtEnd}"
|
|
|
|
[[ "${iDupes}" -gt 0 ]] && printf '%b\n' "${sFmtWarning} There are ${iDupes} duplicate files in /working ${sFmtEnd}"
|
|
|
|
printf '%b\n' "${sFmtQuestion} What would you like to do? ${sFmtEnd}"
|
|
|
|
while true ; do
|
|
select sTask in "${sTask_FileAttribs}" "${sTask_SplitAlbum}" "${sTask_SaveTags}" "${sTask_TagsToFileName}" "${sTask_FormatFileName}" \
|
|
"${sTask_EditFileName}" "${sTask_Decompress}" "${sTask_Integ1}" "${sTask_Spectrogram}" "${sTask_TrimSilence}" "${sTask_Encode}" \
|
|
"${sTask_StripMeta}" "${sTask_RestoreTags}" "${sTask_FileNameToTags}" "${sTask_WriteTag}" "${sTask_Gain}" "${sTask_Optimize}" \
|
|
"${sTask_Integ2}" "${sTask_FindDupes}" "${sTask_FileInfo}" "${sTask_Play}" 'Rotate Files' 'Logging Operations' 'Help' 'Quit'
|
|
do
|
|
case "${sTask}" in
|
|
( "${sTask_FileAttribs}"|"${sTask_SplitAlbum}"|"${sTask_SaveTags}"|"${sTask_TagsToFileName}"|"${sTask_FormatFileName}"| \
|
|
"${sTask_EditFileName}"|"${sTask_Decompress}"|"${sTask_Integ1}"|"${sTask_Spectrogram}"|"${sTask_TrimSilence}"|"${sTask_Encode}"| \
|
|
"${sTask_StripMeta}"|"${sTask_RestoreTags}"|"${sTask_FileNameToTags}"|"${sTask_WriteTag}"|"${sTask_Gain}"|"${sTask_Optimize}"| \
|
|
"${sTask_Integ2}"|"${sTask_FindDupes}"|"${sTask_FileInfo}"|"${sTask_Play}" )
|
|
f_taskInit
|
|
;;
|
|
|
|
( 'Rotate Files' )
|
|
# offer to move files in /working to /finished
|
|
i="$( find -- './' -maxdepth '1' -type 'f' -iname '*.*' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Move ${i} files from /working to /finished? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" != 'n' ]] ; then
|
|
mv --backup='numbered' -- './'* '../finished/'
|
|
s="Moved ${i} files from /working to /finished"
|
|
f_log "${s}"
|
|
printf '%s\n' "${s}"
|
|
fi
|
|
else # offer to move files from specific discard directories to /working
|
|
for sDir in 'holding/' 'discard/metadata_issues/' 'discard/minor_issues/' ; do
|
|
i="$( find -- "../${sDir}" -maxdepth '1' -type 'f' -iname '*.*' | wc -l )" # count objects
|
|
if [[ "${i}" -gt 0 ]] ; then
|
|
printf '%b\n' "${sFmtQuestion} Move ${i} files from ${sDir} to /working? [Y/n] ${sFmtEnd}"
|
|
read -rsN1
|
|
if [[ "${REPLY}" != 'n' ]] ; then
|
|
mv --backup='numbered' -- "../${sDir}/"* './'
|
|
s="Moved ${i} files from ${sDir} to /working"
|
|
f_log "${s}"
|
|
printf '%s\n' "${s}"
|
|
i=1
|
|
f_mainLoop
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "${i}" -eq 0 ]] ; then
|
|
a=(
|
|
"${sFmtNotice} There are no files to move ${sFmtEnd}"
|
|
"${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
)
|
|
printf '%b\n' "${a[@]}"
|
|
read -rsN1
|
|
fi
|
|
|
|
f_mainLoop
|
|
;;
|
|
|
|
( 'Logging Operations' )
|
|
printf '%b\n' "${sFmtQuestion} Select an option ${sFmtEnd}"
|
|
select s in 'Add Log Entry' 'View Log' 'Search Log' 'Cancel'
|
|
do
|
|
case "${s}" in
|
|
( 'Add Log Entry' )
|
|
printf '%b\n' "${sFmtQuestion} Enter the text and press [Enter] ${sFmtEnd}"
|
|
read -re
|
|
if [[ -n "${REPLY}" ]] ; then
|
|
f_log "### USER COMMENT: ${REPLY}"
|
|
printf '%s\n' 'Done!'
|
|
fi
|
|
;;
|
|
|
|
( 'View Log' ) xdg-open '../logs/session.log' &> '/dev/null' ;;
|
|
|
|
( 'Search Log' )
|
|
a=(
|
|
'Prefix the search terms with "N,N/" where "N" equals the number of lines'
|
|
'to display before and after the line containing your search terms. For example:'
|
|
'5,10/your search terms'
|
|
)
|
|
a+=( "${sFmtQuestion} Enter search terms and press [Enter] ${sFmtEnd}" )
|
|
printf '%b\n' "${a[@]}"
|
|
read -re
|
|
if [[ "${REPLY}" = *"'"* ]] ; then
|
|
printf '%b\n' "${sFmtNotice} Search terms may not contain a single quote ${sFmtEnd}"
|
|
else
|
|
sRE='([0-9]+),([0-9]+)\/(.+)'
|
|
if [[ ! "${REPLY}" =~ ${sRE} ]] ; then
|
|
printf '%b\n' "${sFmtNotice} Query string is incorrectly formated ${sFmtEnd}"
|
|
else
|
|
[[ "${iDev}" -eq 0 ]] && clear
|
|
printf '%b\n' "${sFmtHeader} Search Results ${sFmtEnd}"
|
|
if ! grep --color --ignore-case --before-context="${BASH_REMATCH[1]}" --after-context="${BASH_REMATCH[2]}" -- "${BASH_REMATCH[3]}" '../logs/session.log' ; then
|
|
printf '%b\n' "${sFmtNotice} Search terms not found ${sFmtEnd}"
|
|
fi
|
|
fi
|
|
fi
|
|
printf '%b\n' "${sFmtQuestion} Press any key to continue ${sFmtEnd}"
|
|
read -rsN1
|
|
;;
|
|
|
|
( 'Cancel' ) ;;
|
|
|
|
( * ) printf '%b\n' "${sFmtWarning} Invalid choice ${sFmtEnd}" ;;
|
|
esac
|
|
f_mainLoop
|
|
done
|
|
;;
|
|
|
|
( 'Help' )
|
|
printf '%b\n' "${sFmtQuestion} Select an option ${sFmtEnd}"
|
|
select sSel in 'Included Help File' 'Webpage' 'Code Repository' 'Cancel'
|
|
do
|
|
case "${sSel}" in
|
|
( 'Included Help File' ) xdg-open '../README.md' &> '/dev/null' ;;
|
|
( 'Webpage' ) xdg-open "${sWebPg}" ;;
|
|
( 'Code Repository' ) xdg-open "${sRepoURL}" &> '/dev/null' ;;
|
|
( 'Cancel' ) f_mainLoop ;;
|
|
( * ) printf '%b\n' "${sFmtWarning} Invalid choice ${sFmtEnd}" ;;
|
|
esac
|
|
f_mainLoop
|
|
done
|
|
;;
|
|
|
|
( 'Quit' )
|
|
f_houseKeeping 'closeSession'
|
|
a=(
|
|
'' 'Feeling appreciative?' ''
|
|
'Bitcoin: 169GsE9MxH8xn6dpwbwvwxqEER5dwAneKM'
|
|
'Monero: 44uuKLw4VCUMtnf3LLEnRQRX93NYdrcQq8PBXGhPs1SNd3t5ZAWaX5ThZw6hu9YHvTWKjq2KwyXH11RePhrjez9bT5Kovg8'
|
|
'' 'Bye bye :)' ''
|
|
)
|
|
printf '%s\n' "${a[@]}" ; exit
|
|
;;
|
|
|
|
( * ) printf '%b\n' "${sFmtWarning} Invalid choice ${sFmtEnd}" ;;
|
|
esac
|
|
f_mainLoop
|
|
done
|
|
done
|
|
}
|
|
|
|
f_scriptInit
|
|
|
|
exit 1
|