Skip to content

Instantly share code, notes, and snippets.

@berturion
Last active June 19, 2024 06:46
Show Gist options
  • Save berturion/5377d6653ef93049f4ff54cff2003e11 to your computer and use it in GitHub Desktop.
Save berturion/5377d6653ef93049f4ff54cff2003e11 to your computer and use it in GitHub Desktop.
Encode flac files to opus preserving folder structure and stripping ID3 tags
#!/bin/bash
# Copyright 2019 Bertrand Michas <berturion@free.fr>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCNUMBERLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
# "global" variables
USER=$(whoami)
SCRIPT=`realpath $0`
SCRIPT_FILE_NAME=`basename $0`
DIR=`dirname $SCRIPT`
DATEEXEC=`date "+%Y/%m/%d %T"`
# params related variables
ID3_STRIP_ACTION=0
REMOVE_EXISTING_ACTION=0
INPUT_FOLDER=""
OUTPUT_FOLDER=""
BITRATE=0
MIN_BITRATE=16
MAX_BITRATE=192
N=1
MIN_JOBS=1
MAX_JOBS=32
function usage {
cat <<EOF
HELP for $SCRIPT_FILE_NAME:
------------
This script can do multiple actions on flac files.
- Collect all flac files found recursively in a folder to a file
- Remove any ID3 tags in place (ID3 tags are invalid for flac files).
This action modifies the files but preserve all metatada.
- Encode all flac files found into the "input_folder" to the
"output_folder", folder structure is preserved.
- Delete opus encoded target files if exist
Usage: $SCRIPT_FILE_NAME -i input_folder [options]
Options:
-h Show help
-i [required] "input_folder": input folder containing flac
files.
-c [optional] "clear_id3": clear all ID3 tags from flac
files preserving metadata (ID3 tags for flac files are invalid).
-o [optional] "output_folder": output folder where encoded opus
files will be stored. Required with options "-d" or "-b".
-d [optional] "delete_existing": delete existing opus files
in output_folder.
-b [optional] "bitrate": encode flac files from "input_folder"
to opus files into the "output_folder" with the specified vbr
bitrate. Must be between $MIN_BITRATE and $MAX_BITRATE.
Encoding will fail if an ID3 tag is present or if the opus
encoded file already exists.
-j [optional] "jobs": number of parallel jobs. Between
$MIN_JOBS and $MAX_JOBS. Default $N.
Examples :
'$SCRIPT_FILE_NAME -i "path/to/flacs/" -o "path/to/opus/" -d -c -b 96 -j 4
'$SCRIPT_FILE_NAME -h'
Requirements :
flac, opus-tools, id3v2
EOF
}
which metaflac >> /dev/null
if [ $? -ne 0 ]; then
echo "flac tools are not installed. Run \`sudo pacman -S flac\` and try again."
exit 1
fi
which opusenc >> /dev/null
if [ $? -ne 0 ]; then
echo "opus-tools is not installed. Run \`sudo pacman -S opus-tools\` and try again"
exit 1
fi
which id3v2 >> /dev/null
if [ $? -ne 0 ]; then
echo "id3v2 is not installed. Run \`sudo pacman -S id3v2\` and try again"
exit 1
fi
# test that argument is a folder
# if this is not a folder, returns an empty string
# adds a trailing slash if missing
sanitize_folder() {
local folder="$1"
if [ -z "$1" ]; then
folder=""
elif [ ! -d $1 ]; then
folder=""
elif [[ ! "$1" == */ ]];then
folder="$folder/"
fi
echo $folder
}
while getopts "hi:co:edb:j:" opt; do
case $opt in
h)
usage
exit 0
;;
i)
# INPUT_FOLDER=`realpath $OPTARG`
INPUT_FOLDER=`sanitize_folder $OPTARG`
if [ ! -d "$INPUT_FOLDER" ];then
echo "-i \"$OPTARG\" is not a folder"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ ! -w "$INPUT_FOLDER" ];then
echo "-i \"$OPTARG\" is not writable"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
;;
c)
ID3_STRIP_ACTION=1
;;
o)
# OUTPUT_FOLDER=`realpath $OPTARG`
OUTPUT_FOLDER=`sanitize_folder $OPTARG`
if [ ! -d "$OUTPUT_FOLDER" ];then
echo "-o \"$OPTARG\" is not a folder"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ ! -w "$OUTPUT_FOLDER" ];then
echo "-o \"$OPTARG\" is not writable"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
;;
d)
REMOVE_EXISTING_ACTION=1
;;
b)
BITRATE=$(sed 's/[^0-9]//g' <<< $OPTARG)
if [ -z $BITRATE ]; then
echo "-b \"$OPTARG\" invalid bitrate"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ $BITRATE -gt $MAX_BITRATE ]; then
echo "-b \"$OPTARG\" greater than $MAX_BITRATE"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ $BITRATE -lt $MIN_BITRATE ]; then
echo "-b \"$OPTARG\" lesser than $MIN_BITRATE"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
;;
j)
N=$(sed 's/[^0-9]//g' <<< $OPTARG)
if [ -z $N ]; then
echo "-j \"$OPTARG\" invalid number of jobs"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ $N -lt $MIN_JOBS ]; then
echo "-j \"$OPTARG\" lesser than $MIN_JOBS"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ $N -gt $MAX_JOBS ]; then
echo "-j \"$OPTARG\" greater than $MAX_JOBS"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
;;
\?)
echo "Invalid option: -$OPTARG"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
;;
:)
echo "Option -$OPTARG requires an argument."
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
;;
esac
done
# Handle missing required params
if [ -z $OUTPUT_FOLDER ] ; then
if [ $BITRATE -gt 0 ] ; then
echo "Output folder is required to encode the files"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
if [ $REMOVE_EXISTING_ACTION -gt 0 ] ; then
echo "Output folder is required to remove existing encoded files"
echo "Call '$SCRIPT_FILE_NAME -h' to see usage"
exit 1
fi
fi
echo "Enabled actions:"
echo "- Collect flac files"
if [ $BITRATE -gt 0 ];then
echo "- Encode with a bitrate of $BITRATE"
fi
if [ $REMOVE_EXISTING_ACTION -gt 0 ];then
echo "- Remove encoded existing encoded file"
fi
if [ $ID3_STRIP_ACTION -gt 0 ];then
echo "- Strip ID3 tags from flac files"
fi
echo "Parameters:"
echo "- Input folder: $INPUT_FOLDER"
echo "- Output folder: $OUTPUT_FOLDER"
echo "- Number of parallel jobs: $N"
COLLECTED_FLAC_FILES_FILE="${INPUT_FOLDER}collected_flac_files.txt"
rm -f "$COLLECTED_FLAC_FILES_FILE"
# Process flac file with chosen actions
# params:
# - "$1" : input flac file
process_flac() {
flac_file="$1"
echo "Processing $flac_file"
if [[ "$flac_file" == *.flac ]];then
if [ $ID3_STRIP_ACTION -eq 1 ];then
head_bytes=`dd if="$flac_file" of=/dev/stdout bs=1 count=3 2>/dev/null`
if [ "$head_bytes" = "ID3" ];then
echo "Invalid ID3 tag in this .flac file."
metaflac_file="${flac_file}.metaflac"
# Remove invalid ID3 tag from flac file
metaflac --export-tags-to="$metaflac_file" "$flac_file"
id3v2 -D "$flac_file"
metaflac --remove-all-tags --import-tags-from="$metaflac_file" "$flac_file"
rm "$metaflac_file"
fi
fi
new_file="${1/$INPUT_FOLDER/$OUTPUT_FOLDER}"
new_file="${new_file/%flac/opus}"
new_folder="$(dirname "$new_file")"
if [[ -f "$new_file" ]] && [[ $REMOVE_EXISTING_ACTION -eq 1 ]]; then
rm -v "$new_file"
fi
if [ $BITRATE -gt 0 ];then
if [ ! -f "$new_file" ]; then
mkdir -p "$new_folder" && opusenc --vbr --bitrate "$BITRATE" "$flac_file" "$new_file" 2> /dev/null
if [ $? -gt 0 ] ; then
echo "Error encoding $flac_file"
else
echo "Encoded into $new_file"
fi
else
echo "Encoded file already exists: $new_file"
fi
fi
fi
}
# Recursive walk through directories and put each flac file found into
# "$COLLECTED_FLAC_FILES_FILE" file
recurse_conv() {
for i in "$1"/* ; do
if [ -d "$i" ];then
recurse_conv "$i"
elif [ -f "$i" ]; then
found_file="$i"
if [[ $found_file == *.flac ]];then
NB_FLAC_FILES=$NB_FLAC_FILES+1
echo $found_file >> "$COLLECTED_FLAC_FILES_FILE"
fi
fi
done
}
# @see https://unix.stackexchange.com/questions/103920/parallelize-a-bash-for-loop
open_sem(){
mkfifo pipe-$$
exec 3<>pipe-$$
rm pipe-$$
local i=$1
for((;i>0;i--)); do
printf %s 000 >&3
done
}
run_with_lock(){
local x
read -u 3 -n 3 x && ((0==x)) || exit $x
(
( "$@"; )
printf '%.3d' $? >&3
)&
}
echo "Recursive flac collection into $COLLECTED_FLAC_FILES_FILE"
# Removing all trailing slashes before recursive loop in folders
TMP_INPUT_FOLDER=`echo "$INPUT_FOLDER" | sed 's/\/*$//'`
recurse_conv "$TMP_INPUT_FOLDER"
if [[ $BITRATE -gt 0 ]] || [[ $REMOVE_EXISTING_ACTION -gt 0 ]] || [[ $ID3_STRIP_ACTION -gt 0 ]]; then
echo "Parallel processing"
open_sem $N
while read p; do
run_with_lock process_flac "$p"
done <"$COLLECTED_FLAC_FILES_FILE"
fi
# wait until all parallel jobs are finished
wait
echo "Done"
exit 0
@enlose
Copy link

enlose commented Apr 17, 2024

hi! thanks so much for this script (and thank you for keeferrourke for creating this script).
so, the script has worked well for me however there is this bug where if your folder or file names contain non-single white spaces, for example: "Chocolate Strawberry" instead of "Chocolate Strawberry", the script will fail to work. i wonder if you've also encountered this issue in the past and if so do you happen to have the script without this bug? sorry for asking you this. I'm not smart enough to really understand the script and fix it by myself so I wonder if you can help me on this.
once again thank you so much!

@enlose
Copy link

enlose commented Apr 17, 2024

oops it seems Github has automatically removed the double white spaces there lol but you probably get the idea

@berturion
Copy link
Author

Hello,
Sorry, I haven't used this script for several years.
If I have time I'll have a look at it sometime. I can't promise anything though, I'm pretty busy at the moment.
Have a nice day.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment