#!/bin/sh VERSION=1.4.5 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=' ' export POSIXLY_CORRECT=1 if [ -n "${NO_COLOR:-}" ]; then RED="" GREEN="" YELLOW="" RESET="" fi errecho() { >&2 echo "$*$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 C-c to quit' read -r __ fi } _tempdir() { set +u [ -n "$TMPDIR" ] || \ { [ -n "$TEMP" ] && TMPDIR="$TEMP"; } || \ { [ -n "$TMP" ] && TMPDIR="$TMP"; } || \ { [ -d "/tmp" ] && TMPDIR="/tmp"; } || \ { [ -d "/var/tmp" ] && TMPDIR="/var/tmp"; } || \ { [ -d "/usr/tmp" ] && TMPDIR="/usr/tmp"; } || \ TMPDIR="$PWD" set -u } checkupdate() { errecho "you are running version ${GREEN}${VERSION}" errecho "until I learn if forgejo has an equivalent for github's latest release download shenanigans just check to see if this version matches the latest release" errecho "https://git.unix.dog/yosh/flacconv/releases/latest" 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="$TMPDIR/${bname%.*}_IMG" 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 flacconv $VERSION 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. if omitted, the current directory is used 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 output bitrate in kbits (default 128) this value is variable for opus & CBR for mp3 -l mp3 only: use specified mp3 variable quality (0-9). integer only ${YELLOW}OVERRIDES -b${RESET} -k keep specified flac metadata KEYS in output file keys can be checked with metaflac --export-tags-to=- FILE option argument is a ${YELLOW}PIPE-separated${RESET} list of keys to keep, 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 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 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 _tempdir # set temp dir 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="$OPTARG" ;; r) REMOVEKEYS="$OPTARG" ;; 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)' ; } # this script assumes you aren't using newlines in path names # I do not want to change it to account for this # you should never put newlines in paths # it is a very bad idea for many programs 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 "$TMPDIR/flacconvXXXX")" 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 run_in_parallel process_file "$file" done <<-EOF $FLACFILES EOF wait