309 lines
10 KiB
Bash
Executable File
309 lines
10 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
RED="$(printf '\033[38;5;9m')"
|
|
GREEN="$(printf '\033[38;5;10m')"
|
|
YELLOW="$(printf '\033[38;5;11m')"
|
|
RESET="$(printf '\033[m')"
|
|
set -euf
|
|
BN="${0##*/}"
|
|
NL='
|
|
'
|
|
if [ -n "${NO_COLOR:-}" ]; then
|
|
RED="" GREEN="" YELLOW="" RESET=""
|
|
fi
|
|
|
|
errecho() {
|
|
>&2 printf '%s\n' "$*$RESET"
|
|
}
|
|
|
|
fail() {
|
|
errecho "${RED}error: $BN: $RESET$*"
|
|
exit 1
|
|
}
|
|
|
|
warn() {
|
|
errecho "${YELLOW}warning: $BN: $*"
|
|
if [ -z "$IGNOREWARNINGS" ]; then
|
|
errecho 'you can ignore stopping for warnings in the future with -i'
|
|
errecho 'press enter to continue, or Ctrl-c to quit'
|
|
read -r __
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
[ -f "${tmpupdate:-}" ] && rm "$tmpupdate"
|
|
:
|
|
}
|
|
trap "cleanup" INT HUP QUIT TERM EXIT
|
|
|
|
checkupdate() {
|
|
errecho "checking for updates from https://git.unix.dog/yosh/flacconv/src/branch/main/flacconv..."
|
|
errecho
|
|
tmpupdate=$(mktemp)
|
|
curl -s -o "$tmpupdate" 'https://git.unix.dog/yosh/flacconv/raw/branch/main/flacconv' || fail "curl request failed! retry probably"
|
|
if cmp -s "$tmpupdate" "$0"; then
|
|
errecho "you are updated!"
|
|
else
|
|
errecho "the latest commit differs from the current version. if you use git, please git pull the repo for the update"
|
|
errecho "you can see the changes here: https://git.unix.dog/yosh/flacconv/commits/branch/main"
|
|
printf '%s' "if you don't use git, apply update directly? [y/n] " >&2
|
|
read -r choice
|
|
case "$choice" in
|
|
[Yy])
|
|
if [ -L "$0" ]; then
|
|
errecho "${RED}your flacconv patch is a symbolic link."
|
|
errecho "while it is possible to resolve them in a POSIX manner, it's a weird amount of code"
|
|
errecho "so I humbly ask that you update it manually"
|
|
exit 1
|
|
fi
|
|
mv -f "$tmpupdate" "$0"
|
|
errecho "updated!"
|
|
;;
|
|
*) errecho "not applying update!" ;;
|
|
esac
|
|
fi
|
|
exit 0
|
|
}
|
|
|
|
metaflac() { command metaflac --no-utf8-convert "$@"; } # don't wanna fuck up any tags from locale shit
|
|
|
|
convwarn() { errecho "${RED}an error occured while encoding ${RESET}$infile${RED}, skipping and not deleting" && err=1 && DELETE= ; }
|
|
|
|
_opusenc() {
|
|
opusenc --discard-comments \
|
|
"$@" \
|
|
${pic:+--picture "${pic}"} \
|
|
--bitrate "${BITRATE}k" \
|
|
"$infile" \
|
|
"${infile%.*}.opus"
|
|
}
|
|
|
|
_mp3enc() {
|
|
flac --decode --stdout "$infile" | lame \
|
|
-q 0 \
|
|
${cbr:--V "${VLVL}"} \
|
|
--add-id3v2 \
|
|
--tt "${title#*=}" \
|
|
--ta "${artist#*=}" \
|
|
--tl "${album#*=}" \
|
|
--ty "${year#*=}" \
|
|
--tn "${tracknumber#*=}" \
|
|
--tg "${genre#*=}" \
|
|
--tc "${comment#*=}" \
|
|
--tv "TPE2=${albumartist#*=}" \
|
|
${pic:+--ti "${pic}"} \
|
|
- \
|
|
"${infile%.*}.mp3"
|
|
}
|
|
|
|
process_file() {
|
|
infile="$1"
|
|
bname="${infile##*/}"
|
|
|
|
# tagnames will be used for all non-metaflac operations, as it allows for multiline comment shit
|
|
tagnames="$(metaflac --export-tags-to=- "$infile" | grep -E '^[A-Z]+=' | cut -d '=' -f 1 | sort | uniq)"
|
|
|
|
# check if flac has a picture, if not, set RMPIC
|
|
metaflac --export-picture-to=- "$infile" 1>/dev/null 2>&1 || RMPIC=1
|
|
|
|
# first detect any potential ID3 tags
|
|
head_bytes="$(dd if="$infile" bs=1 count=3 2>/dev/null)"
|
|
if [ "$head_bytes" = "ID3" ]; then
|
|
warn "invalid id3 tags found in $infile, you should get that fixed!"
|
|
# in a previous version this would remove invalid id3 tags automatically
|
|
# I have since realized that this is outside the scope of this script
|
|
# additionally, some people may have tags only in the id3 values, and as
|
|
# such would be lost forever if removed automatically
|
|
# therefore, this script only detects id3 tags and alterts the user
|
|
fi
|
|
|
|
# now test if keepkeys or removekeys was used, set exported tags
|
|
if [ -n "$KEEPKEYS" ]; then
|
|
tagnames="$(printf '%s' "$tagnames" | grep -i -E "^$KEEPKEYS")"
|
|
fi
|
|
if [ -n "$REMOVEKEYS" ]; then
|
|
[ "$REMOVEKEYS" = "ALL" ] && REMOVEKEYS=''
|
|
tagnames="$(printf '%s' "$tagnames" | grep -v -i -E "^$REMOVEKEYS")"
|
|
fi
|
|
|
|
# export pic if we're keeping it
|
|
if [ -z "$RMPIC" ]; then
|
|
pic="$(mktemp "${bname%.*}XXXX")"
|
|
metaflac --export-picture-to="$pic" "$infile"
|
|
fi
|
|
|
|
# time to encode
|
|
if [ "$TYPE" = "opus" ]; then
|
|
VLVL=
|
|
# build opus comment chain, can't "import" tags like with flac
|
|
set --
|
|
[ -n "$tagnames" ] && while read -r name; do
|
|
tag="$(metaflac --show-tag="$name" "$infile")"
|
|
# need a while loop here to handle tags with multiple values
|
|
while [ "$tag" != "${tag#*$name=}" ]; do # check if we're on last one
|
|
tag="${tag#*$name=}" # trim prefix
|
|
set -- "$@" "--comment" "$name=${tag%%$NL$name=*}" # set to val without suffix*
|
|
done
|
|
done <<-EOF
|
|
$tagnames
|
|
EOF
|
|
if [ -n "$VERBOSE" ]; then
|
|
_opusenc "$@" || convwarn
|
|
else
|
|
_opusenc "$@" 1>/dev/null 2>&1 || convwarn
|
|
fi
|
|
|
|
# remove encoding metadata
|
|
[ -n "$RMENCTAG" ] && opustags -d ENCODER -d ENCODER_OPTIONS -i "${infile%.*}.opus"
|
|
else
|
|
album="$(metaflac --show-tag=album "$infile")"
|
|
artist="$(metaflac --show-tag=artist "$infile")"
|
|
albumartist="$(metaflac --show-tag=albumartist "$infile")"
|
|
title="$(metaflac --show-tag=title "$infile")"
|
|
year="$(metaflac --show-tag=date "$infile")"
|
|
genre="$(metaflac --show-tag=genre "$infile")"
|
|
tracknumber="$(metaflac --show-tag=tracknumber "$infile")"
|
|
comment="$(metaflac --show-tag=comment "$infile")"
|
|
|
|
[ -z "$VLVL" ] && cbr="-b $BITRATE --cbr"
|
|
if [ -n "$VERBOSE" ]; then
|
|
_mp3enc || convwarn
|
|
else
|
|
_mp3enc 1>/dev/null 2>&1 || convwarn
|
|
fi
|
|
fi
|
|
|
|
[ -n "${pic:-}" ] && rm -f "$pic"
|
|
[ -n "$DELETE" ] && rm -f "$infile"
|
|
[ -z "${err:-}" ] && errecho "$infile ${GREEN}successfully converted to ${RESET}$TYPE${GREEN} with bitrate/quality ${RESET}${VLVL:+V}${VLVL:-${BITRATE}k}"
|
|
}
|
|
|
|
usage() {
|
|
cat >&2 <<-EOF
|
|
usage: $BN [-huvVipe3] [-b BITRATE] [-l LEVEL] [-k KEYS] [-r KEYS] [-j THREADS] [--] <DIRECTORY...>
|
|
|
|
$BN recursively converts directories of flac files to opus/mp3
|
|
DIRECTORY can be specified multiple times
|
|
by default, this script outputs opus with variable bitrate 128k
|
|
${RED}IF ENCODING TO MP3, -k AND -r WILL NOT WORK.${RESET} the only metadata that will be kept is the following:
|
|
${YELLOW}TITLE, ARTIST, ALBUM, ALBUMARTIST, DATE, GENRE, TRACKNUMBER, COMMENT, and the cover picture${RESET}
|
|
(blame id3. just use opus)
|
|
|
|
-h show script help
|
|
-u check for updates
|
|
-v verbose output ${YELLOW}(messy with multithreading)${RESET}
|
|
-V very verbose output ${RED}(VERY messy, use only for debugging and with like, -j 1)${RESET}
|
|
-i ignore script-stopping warnings
|
|
-d delete original flac files after transcoding
|
|
-3 switch output filetype to mp3
|
|
-b <BITRATE> output bitrate in kbits (default 128)
|
|
this value is variable for opus & CBR for mp3
|
|
-l <LEVEL> mp3 only: use specified mp3 variable quality (0-9). integer only
|
|
${YELLOW}OVERRIDES -b${RESET}
|
|
-k <KEYS> keep specified flac metadata KEYS in output file
|
|
keys can be seen using metaflac --export-tags-to=- file.flac
|
|
argument is a ${YELLOW}comma or pipe-separated${RESET} list of keys, case-insensitive
|
|
(i.e. -k "artist,title,albumartist,album,date")
|
|
${YELLOW}if both -k and -r are not present, all keys are kept.${RESET}
|
|
-r <KEYS> remove specified flac metadata KEYS in output file
|
|
option argument is of the same format as -k
|
|
${YELLOW}if set to "ALL" (with capitalization), all keys are removed${RESET}
|
|
-p remove embedded picture in output files
|
|
-e remove the "encoder" tag that automatically gets applied with opusenc
|
|
(requires opustags)
|
|
-j <THREADS> use the specified amount of threads for parallel processing
|
|
if omitted, CPU core count will be used
|
|
EOF
|
|
}
|
|
|
|
VERBOSE=
|
|
REALLYVERBOSE=
|
|
DELETE=
|
|
VLVL=
|
|
RMPIC=
|
|
RMENCTAG=
|
|
KEEPKEYS=
|
|
REMOVEKEYS=
|
|
IGNOREWARNINGS=
|
|
TYPE=opus
|
|
BITRATE=128
|
|
THREADS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null)" # hopefully portable enough to not be an issue
|
|
[ $? -ne 0 ] && warn 'unable to find number of cores! defaulting to 1 thread unless otherwise specified...' && THREADS=1
|
|
|
|
while getopts :huvVid3peb:l:k:r:j: OPT; do
|
|
case "$OPT" in
|
|
h) usage && exit 0 ;;
|
|
u) checkupdate ;;
|
|
v) VERBOSE=1 ;;
|
|
V) VERBOSE=1; REALLYVERBOSE=1 ;;
|
|
i) IGNOREWARNINGS=1 ;;
|
|
d) DELETE=1 ;;
|
|
3) TYPE=mp3 ;;
|
|
p) RMPIC=1 ;;
|
|
e) RMENCTAG=1 ;;
|
|
b) BITRATE="$OPTARG" ;;
|
|
l) VLVL="$OPTARG" ;;
|
|
k) KEEPKEYS="$(printf '%s' "$OPTARG" | tr ',' '|')" ;;
|
|
r) REMOVEKEYS="$(printf '%s' "$OPTARG" | tr ',' '|')" ;;
|
|
j) THREADS="$OPTARG" ;;
|
|
*) fail "unknown option: -$OPTARG. run $BN -h to see all options" ;;
|
|
esac
|
|
done
|
|
shift "$((OPTIND - 1))"
|
|
|
|
[ -n "$REALLYVERBOSE" ] && set -x
|
|
|
|
command -v metaflac 1>/dev/null 2>&1 || fail 'flac tools are not installed! (metaflac)'
|
|
if [ "$TYPE" = "opus" ]; then
|
|
command -v opusenc 1>/dev/null 2>&1 || fail 'opus-tools is not installed! (opusenc)'
|
|
else
|
|
command -v lame 1>/dev/null 2>&1 || fail 'lame is not installed! (lame)'
|
|
fi
|
|
[ -n "$RMENCTAG" ] && { command -v opustags 1>/dev/null 2>&1 || fail 'opustags is not installed! this is required for -e (opustags)' ; }
|
|
|
|
# if no directory provided, show usage
|
|
[ "$#" -eq 0 ] && usage && errecho "${NL}to convert the current working directory, use \"flacconv .\"" && exit 1
|
|
# this script assumes you aren't using newlines in path names
|
|
# I do not want to change it to account for this
|
|
FLACFILES="$(find "$@" -type f -name "*.[fF][lL][aA][cC]")"
|
|
[ -z "$FLACFILES" ] && fail 'no flac files found!'
|
|
|
|
# make a fifo/fd for parallel stuff
|
|
mk_parallel_fd() {
|
|
fifo_para="$(mktemp -u -t "flacconv.XXXX")"
|
|
mkfifo "$fifo_para"
|
|
exec 9<>"$fifo_para"
|
|
rm -f "$fifo_para"
|
|
while [ "$THREADS" -gt 0 ]; do
|
|
printf "\n" >&9 # start with THREADS amount of lines in fd 9 for later
|
|
THREADS="$((THREADS - 1))"
|
|
done
|
|
}
|
|
|
|
# read each line from fd 9, launch new program for each line
|
|
# print a line after program finished such that another one can take its place
|
|
run_in_parallel() {
|
|
cmd="$1"
|
|
shift
|
|
read -r __ <&9
|
|
{
|
|
"$cmd" "$@"
|
|
printf '\n' >&9
|
|
} &
|
|
}
|
|
|
|
errecho "${YELLOW}using ${RESET}$THREADS${YELLOW} threads"
|
|
mk_parallel_fd
|
|
while read -r file; do
|
|
if ! [ -f "$file" ]; then
|
|
errecho "${YELLOW}skipping the file ${RESET}"$file"${YELLOW} because it doesn't exist."
|
|
errecho "${YELLOW}this most likely means you have a newline in the pathname. fix that!"
|
|
continue
|
|
fi
|
|
run_in_parallel process_file "$file"
|
|
done <<-EOF
|
|
$FLACFILES
|
|
EOF
|
|
|
|
wait
|