#!/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] [--] $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 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 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 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 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