flacconv/flacconv

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