#!/bin/bash # # Music library convert and sync. # # For suggestion and bug reports, please contact # Pierre-Yves Landuré # # Thanks to m31z0nyx on irc.freenode.net #debian-facile for his help i # finding a script name. # # Thanks to GNU Parallel. # O. Tange (2011): GNU Parallel - The Command-Line Power Tool, # ;login: The USENIX Magazine, February 2011:42-47. # version="1.7.3" # Known bugs # ---------- # # - M4A to MP3 conversion does not preserve album art. # - M4A to MP3 conversion does not preserver MusicBrainz tags. # - input format detection for copy of same format input to output is broken. # # History. # ------- # # 1.7.3: # - Fix OGG to M4A MusicBrainz tags support. # # 1.7.2: # - Allow conversion of WMA files. # # 1.7.1: # - Fix FLAC to m4a transcoding when FLAC is HD (192k). # # 1.7.0: # - Use aacgain instead of collection gain for m4a files. # - Fix MP3 to M4A MusicBrainz tags support. # - Fix MP3 cover art extraction. # - Add beginning of support for multiformat MusicBrainz tags translation. # # 1.6.2: # - Fix JSON tag info bad cleanup for FLAC to m4a conversion. # - Fix FLAC or OGG to m4a MusicBrainz tag preservation. # # 1.6.1: # - Fix null byte warning in basename and dirname. # # 1.5.1: # - Update check_binary, realpath and realpath_check functions with better result returning. # # 1.5.0: # - fix bug #4. application/octect-stream audio files are now accepted. # - fix bug #5. optionnal arguments were ignored while parsing command line. # - fix feature request #6 add support for aac m4a output. # - use shellcheck to sanitize code. # - use collectiongain from package python-rgain instead of vorbisgain and mp3gain. # - add missing vbrfix dependency # # Get the basename of a path (multi-plateform version) # Print the result on &1 if found. # # @param string $path A path. # # @return A return code.. function basename() { [[ ${#} -eq 0 ]] && exit 1 case "$(uname)" in 'Linux' ) command basename -z -- "${@}" \ | command tr -d '\0' ;; 'Darwin' | * ) command basename -- "${@}" ;; esac return ${?} } # basename() export -f 'basename' script_name="$(basename "${0}")" # Print this script help. function usage { echo "Music library convert and sync v${version} This tool convert a input (Flac) music library to an output (MP3) music folder and keep the folder structure. Usage : ${script_name} [ --help ] [ --quiet ] [ --verbose ] --input-path='/path/to/original/library' [ --output-path='/path/to/converted/library' ] [ --input-format='*' ] [ --output-format='mp3' ] [ --bitrate='320k' | --quality=5 ] [ --sync-delete ] [ --copy-all ] Available options are : * --help | -h : Display this message. * --input-path | --in | -i : Set the path to the source library (mandatory) * --output-path | --out | -o : Set the path to the target library (default to source library) * --input-format | --if : Set the input format. Default to *. * --output-format | --of | -e : Set the output format. Default to MP3. * --bitrate | -b : Set the CBR bitrate. Default to 192k. Trigger CBR encoding. * --quality | -q : Set the VBR quality. Default to 5. 0 is the lowest, 9 the highest. Trigger VBR encoding (default). * --replaygain | --gain | -g : Apply album replay gain to converted files. * --sync-delete | --delete | -d : Propagate file deletion from input library to output library. Any output file without equivalence in input library is deleted. * --copy-all | --copy | -c : Copy non audio files found in source library to target library. * --threads | -t : Threads number. Default to 1 thread by CPU. * --quiet | --silent | -s : Disable almost all outputs. * --verbose | -v : Enable debug outputs. Completly supported formats are : * MP3 (mp3) * Flac (flac) * Ogg/Vorbis (ogg) * m4a (aac) When converting from/to this formats, metadatas and cover art are transfered to the created files. " [[ -n "${1}" ]] && exit "${1}" } # usage # Get the dirname of a path (multi-plateform version) # Print the result on &1 if found. # # @param string $path A path. # # @return A return code.. function dirname() { [[ ${#} -eq 0 ]] && exit 1 case "$(uname)" in 'Linux' ) command dirname -z -- "${@}" \ | command tr -d '\0' ;; 'Darwin' | * ) command dirname -- "${@}" ;; esac return ${?} } # dirname() export -f 'dirname' # Get the absolute path for a file or directory. # Print its path on &1 if found. # # @param string $path A relative path. # # @return ${realpath} A absolute path. function realpath() { [[ ${#} -ne 1 ]] && exit 1 local realpath case "$(uname)" in 'Linux' ) realpath="$(readlink -f "${1}")" ;; 'Darwin' ) realpath="$(stat -f '%N' "${1}")" ;; * ) realpath="$(realpath "${1}")" ;; esac echo -n "${realpath}" return 0 } # realpath # Get the absolute path for a file or directory and check the file existance. # If the file does not exists, display an error message and exit the script. # Print its path on &1 if found. # # @param string $path A relative path. # # @return Exit with error if the path is missing. function realpath_check() { [[ ${#} -ne 1 ]] && exit 1 local realpath realpath="$(realpath "${1}")" if [[ -n "${realpath}" && ! -e "${realpath}" ]]; then realpath='' fi if [[ -z "${realpath}" ]]; then cecho 'redbold' "Error: File '${1}' does not exists." >&2 exit 1 fi echo -n "${realpath}" return 0 } # realpath_check # Check if a binary is present. Print its path on &1 if found. # # @param string $binary The binaries to check, separated by ;. # @param string $package The package the binary come from. # # @return Exit with error if the binary is missing. function check_binary() { [[ ${#} -ne 2 ]] && exit 1 local primary local binaries local binary primary="${1%%;*}" binaries=() read -d ';' -r -a binaries <<< "${1}" # Test the binary presence. for binary in "${binaries[@]}"; do if type "${binary}" &>'/dev/null'; then command -v "${binary}" return 0 fi done cecho 'redbold' "Error: '${primary}' is missing. Please install package '${2}'." >&2 exit 1 } # check_binary() # # Multi platform avprobe support. # function avprobe() { if type -f 'avprobe' &>'/dev/null'; then command avprobe "${@}" return ${?} fi if type -f 'ffprobe' &>'/dev/null'; then command ffprobe "${@}" return ${?} fi exit 1 } # avprobe() export -f 'avprobe' # # Multi platform avconv support. # function avconv() { if type -f 'avconv' &>'/dev/null'; then command avconv "${@}" return ${?} fi if type -f 'ffmpeg' &>'/dev/null'; then command ffmpeg "${@}" return ${?} fi exit 1 } # avconv() export -f 'avconv' # # Multi platform mp3gain support. # function mp3gain() { if type -f 'mp3gain' &>'/dev/null'; then command mp3gain "${@}" return ${?} fi if type -f 'aacgain' &>'/dev/null'; then command aacgain "${@}" return ${?} fi exit 1 } # mp3gain() export -f 'mp3gain' # # Multi platform parallel support. # function parallel() { # Detect parallel line based on OS. case "$(uname)" in 'Darwin' ) command parallel --no-notice "${@}" return ${?} ;; 'Linux' | * ) # Nothing to do. command parallel "${@}" return ${?} ;; esac } # parallel() export -f 'parallel' # # Multi platform base64 support. # function base64() { # Detect parallel line based on OS. case "$(uname)" in 'Darwin' ) command base64 --break=0 "${@}" return ${?} ;; 'Linux' | * ) # Nothing to do. command base64 --wrap=0 "${@}" return ${?} ;; esac } # base64() export -f 'base64' # # Multi platform eyed3 support. # function eyed3() { if type -f 'eyeD3' &>'/dev/null'; then eyeD3 "${@}" return ${?} fi if type -f 'eyeD3-2.7' &>'/dev/null'; then eyeD3-2.7 "${@}" return ${?} fi if type -f 'eyeD3-2.6' &>'/dev/null'; then eyeD3-2.6 "${@}" return ${?} fi exit 1 } # eyed3() export -f 'eyed3' # # Multi platform eyed3 remove all support. # function eyed3_remove_all() { # Detect parallel line based on OS. case "$(uname)" in 'Darwin' ) eyed3 --remove-all-images "${@}" return ${?} ;; 'Linux' | * ) # Nothing to do. eyed3 --remove-images "${@}" return ${?} ;; esac } # eyed3_remove_all() export -f 'eyed3_remove_all' # Echo text in color. # # Colors definitions. # See http://mywiki.wooledge.org/BashFAQ/037 # # @param string $color Color and weight for text. (boldgreen for example). # @param string $text The text to echo (and echo options). function cecho() { if [[ ${#} -lt 2 ]]; then echo "${@}" return 0 fi local color="${1}" # remove color information from arguments. shift 1 # Check that the output is to a terminal. if [[ ! -t 1 ]]; then # Not outputing to a terminal, discaring colors. echo "${@}" return 0 fi # Bash 4 version with associative array. ## Color and weight definitions. #declare -A font #font['black']="$(tput 'setaf' 0)" #font['red']="$(tput 'setaf' 1)" #font['green']="$(tput 'setaf' 2)" #font['yellow']="$(tput 'setaf' 3)" #font['blue']="$(tput 'setaf' 4)" #font['magenta']="$(tput 'setaf' 5)" #font['cyan']="$(tput 'setaf' 6)" #font['white']="$(tput 'setaf' 7)" #font['bgBlack']="$(tput 'setab' 0)" #font['bgRed']="$(tput 'setab' 1)" #font['bgGreen']="$(tput 'setab' 2)" #font['bgYellow']="$(tput 'setab' 3)" #font['bgBlue']="$(tput 'setab' 4)" #font['bgMagenta']="$(tput 'setab' 5)" #font['bgCyan']="$(tput 'setab' 6)" #font['bgWhite']="$(tput 'setab' 7)" #font['bold']="$(tput 'bold')" #font['stout']="$(tput 'smso')" # Standout. #font['under']="$(tput 'smul')" # Underline. #font['blink']="$(tput 'blink')" # Blinking #font['italic']="$(tput 'sitm')" ## Parse the color string. #for key in "${!font[@]}"; do # [[ "${color}" = *"${key}"* ]] && echo -n "${font[${key}]}" #done declare -a fontIndex declare -a fontValue local index=0 fontIndex[$index]='black'; fontValue[$index]="$(tput 'setaf' 0)"; ((index++)) fontIndex[$index]='red'; fontValue[$index]="$(tput 'setaf' 1)"; ((index++)) fontIndex[$index]='green'; fontValue[$index]="$(tput 'setaf' 2)"; ((index++)) fontIndex[$index]='yellow'; fontValue[$index]="$(tput 'setaf' 3)"; ((index++)) fontIndex[$index]='blue'; fontValue[$index]="$(tput 'setaf' 4)"; ((index++)) fontIndex[$index]='magenta'; fontValue[$index]="$(tput 'setaf' 5)"; ((index++)) fontIndex[$index]='cyan'; fontValue[$index]="$(tput 'setaf' 6)"; ((index++)) fontIndex[$index]='white'; fontValue[$index]="$(tput 'setaf' 7)"; ((index++)) fontIndex[$index]='bgBlack'; fontValue[$index]="$(tput 'setab' 0)"; ((index++)) fontIndex[$index]='bgRed'; fontValue[$index]="$(tput 'setab' 1)"; ((index++)) fontIndex[$index]='bgGreen'; fontValue[$index]="$(tput 'setab' 2)"; ((index++)) fontIndex[$index]='bgYellow'; fontValue[$index]="$(tput 'setab' 3)"; ((index++)) fontIndex[$index]='bgBlue'; fontValue[$index]="$(tput 'setab' 4)"; ((index++)) fontIndex[$index]='bgMagenta'; fontValue[$index]="$(tput 'setab' 5)"; ((index++)) fontIndex[$index]='bgCyan'; fontValue[$index]="$(tput 'setab' 6)"; ((index++)) fontIndex[$index]='bgWhite'; fontValue[$index]="$(tput 'setab' 7)"; ((index++)) fontIndex[$index]='bold'; fontValue[$index]="$(tput 'bold')"; ((index++)) fontIndex[$index]='stout'; fontValue[$index]="$(tput 'smso')"; ((index++)) # Standout. fontIndex[$index]='under'; fontValue[$index]="$(tput 'smul')"; ((index++)) # Underline. fontIndex[$index]='blink'; fontValue[$index]="$(tput 'blink')"; ((index++)) # Blinking. fontIndex[$index]='italic'; fontValue[$index]="$(tput 'sitm')"; ((index++)) for key in "${!fontIndex[@]}"; do [[ "${color}" = *"${fontIndex[${key}]}"* ]] && echo -n "${fontValue[${key}]}" done # Output the text. echo "${@}" # Reset all attributes. tput 'sgr0' return 0 } # cecho() export -f 'cecho' # Translage short tags code to other tag name for chosen audio format. # @see https://picard.musicbrainz.org/docs/mappings/ # # @param string $inputFileMimeType Mime Type of the original audio file. # @param string $outputFormat Target audio format. # @param string $jsonFile Path to tags json file. function translate_short_tags() { [[ ${#} -ne 3 ]] && exit 1 local inputFileMimetype="${1}" local outputFormat="${2}" local jsonFile="${3}" declare -a mp3TagNames declare -a flacTagNames declare -a m4aTagNames mp3TagNames=() flacTagNames=() m4aTagNames=() mp3TagNames+=('TSO2') flacTagNames+=('ALBUMARTISTSORT') m4aTagNames+=('ALBUMARTISTSORT') for key in "${!mp3TagNames[@]}"; do local sourceTag='' local targetTag='' case "${inputFileMimetype}" in 'audio/x-flac' | 'audio/ogg' ) sourceTag="${flacTagNames[${key}]}" ;; 'audio/mpeg' ) sourceTag="${mp3TagNames[${key}]}" ;; 'audio/x-m4a' ) sourceTag="${m4aTagNames[${key}]}" ;; esac case "${outputFormat}" in 'flac' | 'ogg' | 'vorbis' ) targetTag="${flacTagNames[${key}]}" ;; 'mp3' ) targetTag="${mp3TagNames[${key}]}" ;; 'm4a' | 'aac' ) targetTag="${m4aTagNames[${key}]}" ;; esac if [[ -n "${sourceTag}" && -n "${targetTag}" ]]; then sed -i -e "s/\"${sourceTag}\"/\"${targetTag}\"/" "${jsonFile}" fi done return 0 } # translate_short_tags() export -f 'translate_short_tags' # Translage MusicBrainz tag code to tag name for m4a file. # @see https://picard.musicbrainz.org/docs/mappings/ # # @param string $inputFileMimeType Mime Type of the original audio file. # @param string $tagCode Original MusicBrainz tag code. function translate_musicbrainz_for_m4a() { [[ ${#} -ne 2 ]] && exit 1 local inputFileMimetype="${1}" local tagCode="${2}" declare -a mp3TagNames declare -a flacTagNames declare -a m4aTagNames local index=0 mp3TagNames=() flacTagNames=() m4aTagNames=() mp3TagNames+=('Artists') mp3TagNames+=('TMED') mp3TagNames+=('SCRIPT') mp3TagNames+=('TEXT') mp3TagNames+=('TPE3') mp3TagNames+=('TPE4') mp3TagNames+=('IPLS:engineer') mp3TagNames+=('TIPL:engineer') mp3TagNames+=('IPLS:producer') mp3TagNames+=('TIPL:producer') mp3TagNames+=('IPLS:DJ-mix') mp3TagNames+=('TIPL:DJ-mix') mp3TagNames+=('IPLS:mix') mp3TagNames+=('TIPL:mix') mp3TagNames+=('TPUB') mp3TagNames+=('publisher') mp3TagNames+=('TIT3') mp3TagNames+=('TSST') mp3TagNames+=('TMOO') mp3TagNames+=('TMED') mp3TagNames+=('CATALOGNUMBER') mp3TagNames+=('MusicBrainz Album Status') mp3TagNames+=('MusicBrainz Album Type') mp3TagNames+=('MusicBrainz Album Release Country') mp3TagNames+=('SCRIPT') mp3TagNames+=('TLAN') mp3TagNames+=('LICENSE') mp3TagNames+=('WCOP') mp3TagNames+=('BARCODE') mp3TagNames+=('TSRC') mp3TagNames+=('ASIN') mp3TagNames+=('MusicBrainz Track Id') # avprobe does not output this value. mp3TagNames+=('MusicBrainz Release Track Id') mp3TagNames+=('MusicBrainz Album Id') mp3TagNames+=('MusicBrainz Artist Id') mp3TagNames+=('MusicBrainz Album Artist Id') mp3TagNames+=('MusicBrainz Release Group Id') mp3TagNames+=('MusicBrainz Work Id') mp3TagNames+=('MusicBrainz TRM Id') mp3TagNames+=('MusicBrainz Disc Id') mp3TagNames+=('Acoustid Id') mp3TagNames+=('Acoustid Fingerprint') mp3TagNames+=('MusicIP PUID') mp3TagNames+=('MusicMagic Fingerprint') flacTagNames+=('ARTISTS') flacTagNames+=('MEDIA') flacTagNames+=('SCRIPT') flacTagNames+=('LYRICIST') flacTagNames+=('CONDUCTOR') flacTagNames+=('REMIXER') flacTagNames+=('ENGINEER') flacTagNames+=('ENGINEER') flacTagNames+=('PRODUCER') flacTagNames+=('PRODUCER') flacTagNames+=('DJMIXER') flacTagNames+=('DJMIXER') flacTagNames+=('MIXER') flacTagNames+=('MIXER') flacTagNames+=('LABEL') flacTagNames+=('LABEL') flacTagNames+=('SUBTITLE') flacTagNames+=('DISCSUBTITLE') flacTagNames+=('MOOD') flacTagNames+=('MEDIA') flacTagNames+=('CATALOGNUMBER') flacTagNames+=('RELEASESTATUS') flacTagNames+=('RELEASETYPE') flacTagNames+=('RELEASECOUNTRY') flacTagNames+=('SCRIPT') flacTagNames+=('LANGUAGE') flacTagNames+=('LICENSE') flacTagNames+=('LICENSE') flacTagNames+=('BARCODE') flacTagNames+=('ISRC') flacTagNames+=('ASIN') flacTagNames+=('MUSICBRAINZ_TRACKID') flacTagNames+=('MUSICBRAINZ_RELEASETRACKID') flacTagNames+=('MUSICBRAINZ_ALBUMID') flacTagNames+=('MUSICBRAINZ_ARTISTID') flacTagNames+=('MUSICBRAINZ_ALBUMARTISTID') flacTagNames+=('MUSICBRAINZ_RELEASEGROUPID') flacTagNames+=('MUSICBRAINZ_WORKID') flacTagNames+=('MUSICBRAINZ_TRMID') flacTagNames+=('MUSICBRAINZ_DISCID') flacTagNames+=('ACOUSTID_ID') flacTagNames+=('ACOUSTID_FINGERPRINT') flacTagNames+=('MUSICIP_PUID') flacTagNames+=('FINGERPRINT=MusicMagic Fingerprint') m4aTagNames+=('ARTISTS') m4aTagNames+=('MEDIA') m4aTagNames+=('SCRIPT') m4aTagNames+=('LYRICIST') m4aTagNames+=('CONDUCTOR') m4aTagNames+=('REMIXER') m4aTagNames+=('ENGINEER') m4aTagNames+=('ENGINEER') m4aTagNames+=('PRODUCER') m4aTagNames+=('PRODUCER') m4aTagNames+=('DJMIXER') m4aTagNames+=('DJMIXER') m4aTagNames+=('MIXER') m4aTagNames+=('MIXER') m4aTagNames+=('LABEL') m4aTagNames+=('LABEL') m4aTagNames+=('SUBTITLE') m4aTagNames+=('DISCSUBTITLE') m4aTagNames+=('MOOD') m4aTagNames+=('MEDIA') m4aTagNames+=('CATALOGNUMBER') m4aTagNames+=('MusicBrainz Album Status') m4aTagNames+=('MusicBrainz Album Type') m4aTagNames+=('MusicBrainz Album Release Country') m4aTagNames+=('SCRIPT') m4aTagNames+=('LANGUAGE') m4aTagNames+=('LICENSE') m4aTagNames+=('LICENSE') m4aTagNames+=('BARCODE') m4aTagNames+=('ISRC') m4aTagNames+=('ASIN') m4aTagNames+=('MusicBrainz Track Id') m4aTagNames+=('MusicBrainz Release Track Id') m4aTagNames+=('MusicBrainz Album Id') m4aTagNames+=('MusicBrainz Artist Id') m4aTagNames+=('MusicBrainz Album Artist Id') m4aTagNames+=('MusicBrainz Release Group Id') m4aTagNames+=('MusicBrainz Work Id') m4aTagNames+=('MusicBrainz TRM Id') m4aTagNames+=('MusicBrainz Disc Id') m4aTagNames+=('Acoustid Id') m4aTagNames+=('Acoustid Fingerprint') m4aTagNames+=('MusicIP PUID') m4aTagNames+=('fingerprint') foundKey='null' case "${inputFileMimetype}" in 'audio/x-flac' | 'audio/ogg' ) for key in "${!flacTagNames[@]}"; do if [[ "${tagCode}" = *"${flacTagNames[${key}]}"* ]]; then foundKey="${key}" break; fi done ;; 'audio/mpeg' ) for key in "${!mp3TagNames[@]}"; do if [[ "${tagCode}" = *"${mp3TagNames[${key}]}"* ]]; then foundKey="${key}" break; fi done ;; 'audio/x-m4a' ) for key in "${!m4aTagNames[@]}"; do if [[ "${tagCode}" = *"${m4aTagNames[${key}]}"* ]]; then foundKey="${key}" break; fi done ;; esac [[ "${foundKey}" != 'null' ]] && echo -n "${m4aTagNames[${foundKey}]}" return 0 } # translate_musicbrainz_for_m4a() export -f 'translate_musicbrainz_for_m4a' # Transfert cover art from input file to output file. # Reset all cover art in output file. # # @param string $input_file The audio input file. # @param string $output_file The audio output file. # # @return Exit with error if conversion failed. function transfert_images() { [[ ${#} -ne 2 ]] && exit 1 local input_file local output_file local input_file_has_cover local image_path local image_with_headers_path local comments_path local mp3_images_path input_file="${1}" output_file="${2}" # Detect if input file has cover art. input_file_has_cover="$(mediainfo --Inform="General;%Cover%" "${input_file}")" # temporary files. image_path="" image_with_headers_path="" comments_path="" mp3_images_path="" #------------------------ # Trap handling function # use with: trap 'exit_transfert_images_on_signal "${output_file}" "${image_path}" "${image_with_headers_path}" "${comments_path}" "${mp3_images_path}"' SIGHUP SIGINT SIGQUIT SIGTERM # # @param string $output_file The converted/copied output file. # @param string $image_path The extracted image temporary file. function exit_transfert_images_on_signal() { local interrupt local output_file local image_path local image_with_headers_path local comments_path local mp3_images_path local output_filename local image_mime_type local image_extension local input_file_format local input_file_audio_format local block_number interrupt=${?} output_file="${1}" image_path="${2}" image_with_headers_path="${3}" comments_path="${4}" mp3_images_path="${5}" output_filename="$(basename "${output_file}")" cecho 'boldred' "Image transfert to '${output_filename}' interrupted." >&2 [[ -n "${output_file}" && -e "${output_file}" ]] && rm "${output_file}" [[ -n "${image_path}" && -e "${image_path}" ]] && rm "${image_path}" [[ -n "${image_with_headers_path}" && -e "${image_with_headers_path}" ]] && rm "${image_with_headers_path}" [[ -n "${comments_path}" && -e "${comments_path}" ]] && rm "${comments_path}" [[ -n "${mp3_images_path}" && -d "${mp3_images_path}" ]] && rm -r "${mp3_images_path}" #local running_jobs=($(jobs -p)) #if [[ -n "${running_jobs}" ]]; then # cecho 'yellow' " -> Stopping conversion jobs (${running_jobs[@]})." >&2 # kill ${running_jobs[@]} 2>&- #fi if [[ ${interrupt} -eq 2 ]]; then # killing self if catched a SIGINT. # see http://mywiki.wooledge.org/SignalTrap trap - SIGINT kill -INT $$ else exit 0 fi } # exit_transfert_images_on_signal() #------------------------ if [[ "${input_file_has_cover}" != 'Yes' ]] \ && grep -q --ignore-case '^METADATA_BLOCK_PICTURE' <<< "$(mediainfo "${input_file}")"; then input_file_has_cover='Yes' fi if [[ "${input_file_has_cover}" = 'Yes' ]]; then echo -n " + Transfering cover art to " cecho 'magenta' -n "$(basename "${output_file}")" echo -n "... " # Setup trap since there is processing needed. trap 'exit_transfert_images_on_signal "${output_file}" "${image_path}" "${image_with_headers_path}" "${comments_path}" "${mp3_images_path}"' SIGHUP SIGINT SIGQUIT SIGTERM # Detect Cover MIME type. image_mime_type="$(mediainfo --Inform="General;%Cover_Mime%" "${input_file}")" image_extension="${image_mime_type#*/}" # Create image temporary file. image_path="$(mktemp -t "tmp.XXXXXXXXXX")" if [[ -n "${image_extension}" ]]; then mv "${image_path}" "${image_path}.${image_extension}" 2>&- image_path="${image_path}.${image_extension}" fi # Detect input file format and audio format. input_file_format="$(mediainfo --Inform="General;%Format%" "${input_file}" | tr '[:lower:]' '[:upper:]')" input_file_audio_format="$(mediainfo --Inform="Audio;%Format%" "${input_file}")" cecho 'blue' -n "Extracting... " case "${input_file_format}" in 'FLAC' ) # File is Flac # List IMAGE metadata blocs. # Assume first bloc is cover art. block_number="$(metaflac --list --block-type='PICTURE' "${input_file}" \ | grep --max-count=1 '^METADATA block' \ | sed -e 's/^.*#//')" # Determine cover image MIME type. image_mime_type="$(metaflac --block-number="${block_number}" --list "${input_file}" \ | grep "MIME type:" \ | sed -e 's/.*MIME type: //g')" # Extract cover image from file. metaflac --block-number="${block_number}" \ --export-picture-to="${image_path}" "${input_file}" ;; 'MPEG AUDIO' ) # File is MP3 mp3_images_path="$(mktemp -d -t "tmp.XXXXXXXXXX")" if [[ "${verbose}" = 'True' ]]; then eyed3 --write-images="${mp3_images_path}" "${input_file}" else eyed3 --write-images="${mp3_images_path}" "${input_file}" &>'/dev/null' fi # Detect cover art. cover_image_file_path="$(find "${mp3_images_path}" -name 'FRONT_COVER.*')" if [[ -e "${cover_image_file_path}" ]]; then # Redetect extension. image_extension="${cover_image_file_path##*.}" mv "${image_path}" "${image_path}.${image_extension}" 2>&- image_path="${image_path}.${image_extension}" mv "${cover_image_file_path}" "${image_path}" 2>&- fi rm -r "${mp3_images_path}" ;; 'MPEG-4' ) # File is M4A / AAC m4a_images_path="$(mktemp -d -t "tmp.XXXXXXXXXX")" if [[ "${verbose}" = 'True' ]]; then AtomicParsley "${input_file}" --extractPixToPath "${m4a_images_path}/" else AtomicParsley "${input_file}" --extractPixToPath "${m4a_images_path}/" &>'/dev/null' fi cover_image_file_path="$(find "${m4a_images_path}" -name '_artwork_1.*')" if [[ -e "${cover_image_file_path}" ]]; then # Redetect extension. image_extension="${cover_image_file_path##*.}" mv "${image_path}" "${image_path}.${image_extension}" 2>&- image_path="${image_path}.${image_extension}" mv "${cover_image_file_path}" "${image_path}" 2>&- fi rm -r "${m4a_images_path}" ;; 'OGG' ) # File is an OGG container. case "${input_file_audio_format}" in 'Vorbis' ) # File is OGG Vorbis. local mime_length local description_skip local description_length local image_length_skip local image_length local image_skip image_with_headers_path="$(mktemp -t "tmp.XXXXXXXXXX")" vorbiscomment --list --raw "${input_file}" \ | grep --ignore-case --max-count=1 '^METADATA_BLOCK_PICTURE=' \ | sed -e 's/^METADATA_BLOCK_PICTURE=//I' \ | base64 --decode \ > "${image_with_headers_path}" # Fetch data lengths. mime_length="$(dd skip=4 count=4 bs=1 \ if="${image_with_headers_path}" 2>&- \ | xxd -p -g0 \ | xargs -IHEX printf "%d" '0xHEX')" image_mime_type="$(dd skip=8 count="${mime_length}" bs=1 \ if="${image_with_headers_path}" 2>&-)" # Now we try to detect extension and create the temporary image file. image_extension="${image_mime_type#*/}" if [[ -z "${image_extension}" ]]; then # Unable to determine cover mime type. [[ -e "${image_with_headers_path}" ]] && rm "${image_with_headers_path}" cecho 'red' "Failed (MIME type unsupported (${image_mime_type}))." return 1 fi image_path="$(mktemp -t "tmp.XXXXXXXXXX")" mv "${image_path}" "${image_path}.${image_extension}" 2>&- image_path="${image_path}.${image_extension}" description_skip="$((8 + mime_length))" description_length="$(dd skip=${description_skip} count=4 bs=1 \ if="${image_with_headers_path}" 2>&- \ | xxd -p -g0 \ | xargs -IHEX printf "%d" '0xHEX')" image_length_skip="$((description_skip + description_length + 20))" image_length="$(dd skip=${image_length_skip} count=4 bs=1 \ if="${image_with_headers_path}" 2>&- \ | xxd -p -g0 \ | xargs -IHEX printf "%d" '0xHEX')" image_skip="$((image_length_skip + 4))" # Extract image. if [[ "${verbose}" = 'True' ]]; then dd skip="${image_skip}" count="${image_length}" bs=1 \ if="${image_with_headers_path}" of="${image_path}" else dd skip="${image_skip}" count="${image_length}" bs=1 \ if="${image_with_headers_path}" of="${image_path}" 2>&- fi # Delete vorbiscomment raw file. [[ -e "${image_with_headers_path}" ]] && rm "${image_with_headers_path}" ;; * ) [[ -e "${image_path}" ]] && rm "${image_path}" cecho 'red' "Failed (Ogg audio encoding unsupported (${input_file_audio_format}))." return 1 ;; esac ;; * ) [[ -e "${image_path}" ]] && rm "${image_path}" cecho 'red' "Failed (Input file format unsupported (${input_file_format}))." return 1 ;; esac # Test if an image has been extracted from input_file and if we have an output_file. if [[ -s "${image_path}" && -e "${output_file}" ]]; then cecho 'blue' -n "Importing... " local output_file_format local output_file_audio_format # Detect output file format and audio format. output_file_format="$(mediainfo --Inform="General;%Format%" "${output_file}" | tr '[:lower:]' '[:upper:]')" output_file_audio_format="$(mediainfo --Inform="Audio;%Format%" "${output_file}")" case "${output_file_format}" in 'FLAC' ) # File is Flac # Remove existing images. metaflac --block-type='PICTURE' --dont-use-padding --remove "${output_file}" # Insert cover image from file. metaflac --import-picture-from="${image_path}" "${output_file}" ;; 'MPEG Audio' | 'MPEG AUDIO' ) # File is MP3 if [[ "${verbose}" = 'True' ]]; then # Remove existing images. eyed3_remove_all "${output_file}" # Insert cover image from file eyed3 --add-image="${image_path}:FRONT_COVER" "${output_file}" else # Remove existing images. eyed3_remove_all "${output_file}" &>'/dev/null' # Insert cover image from file eyed3 --add-image="${image_path}:FRONT_COVER" "${output_file}" &>'/dev/null' fi ;; 'MPEG-4' ) # File is M4A AAC if [[ "${verbose}" = 'True' ]]; then # Remove existing images. AtomicParsley "${output_file}" --artwork REMOVE_ALL --overWrite # Insert cover image from file AtomicParsley "${output_file}" --artwork "${image_path}" --overWrite else # Remove existing images. AtomicParsley "${output_file}" --artwork REMOVE_ALL --overWrite &> '/dev/null' # Insert cover image from file AtomicParsley "${output_file}" --artwork "${image_path}" --overWrite &> '/dev/null' fi ;; 'OGG' ) # File is an OGG container. case "${output_file_audio_format}" in 'Vorbis' ) # File is OGG Vorbis. # Export existing comments to file. comments_path="$(mktemp -t "tmp.XXXXXXXXXX")" vorbiscomment --list --raw "${output_file}" > "${comments_path}" # Remove existing images. sed -i'' -e '/^METADATA_BLOCK_PICTURE/Id' "${comments_path}" # Insert cover image from file. # metadata_block_picture format. # See: https://xiph.org/flac/format.html#metadata_block_picture image_with_headers_path="$(mktemp -t "tmp.XXXXXXXXXX")" local description="" # Reset cache file. echo -n "" > "${image_with_headers_path}" # Picture type <32>. printf "0: %.8x" 3 | xxd -r -g0 \ >> "${image_with_headers_path}" # Mime type length <32>. printf "0: %.8x" ${#image_mime_type} \ | xxd -r -g0 \ >> "${image_with_headers_path}" # Mime type (n * 8) echo -n "${image_mime_type}" >> "${image_with_headers_path}" # Description length <32>. printf "0: %.8x" ${#description} \ | xxd -r -g0 \ >> "${image_with_headers_path}" # Description (n * 8) echo -n "${description}" >> "${image_with_headers_path}" # Picture with <32>. printf "0: %.8x" 0 | xxd -r -g0 \ >> "${image_with_headers_path}" # Picture height <32>. printf "0: %.8x" 0 | xxd -r -g0 \ >> "${image_with_headers_path}" # Picture color depth <32>. printf "0: %.8x" 0 | xxd -r -g0 \ >> "${image_with_headers_path}" # Picture color count <32>. printf "0: %.8x" 0 | xxd -r -g0 \ >> "${image_with_headers_path}" # Image file size <32>. printf "0: %.8x" "$(wc -c "${image_path}" \ | cut -d ' ' -f 1)" \ | xxd -r -g0 \ >> "${image_with_headers_path}" # Image file. cat "${image_path}" >> "${image_with_headers_path}" echo "METADATA_BLOCK_PICTURE=$(base64 < "${image_with_headers_path}")" >> "${comments_path}" # Update vorbis file comments. vorbiscomment --write --raw --commentfile "${comments_path}" "${output_file}" # Delete cache file. [[ -e "${image_with_headers_path}" ]] && rm "${image_with_headers_path}" # Delete comments file. [[ -e "${comments_path}" ]] && rm "${comments_path}" ;; * ) [[ -e "${image_path}" ]] && rm "${image_path}" cecho 'red' "Failed (Ogg audio encoding unsupported (${output_file_audio_format}))." return 1 ;; esac ;; * ) [[ -e "${image_path}" ]] && rm "${image_path}" cecho 'red' "Failed (Output file format unsupported (${output_file_format}))." return 1 ;; esac fi [[ -e "${image_path}" ]] && rm "${image_path}" cecho 'green' "Done." fi return 0 } # transfert_images() export -f 'transfert_images' # Convert an audio file from source library to target library. # # @param string $input_file The audio input file. # # @return Exit with error if conversion failed. function convert_file() { [[ ${#} -ne 1 ]] && exit 1 local input_file="${1}" ## Build output file path based on input file path. local input_file_path local output_file_path local input_filename local output_filename local input_file_mimetype local input_file_brief local input_file_extension local output_file #local short_input_file local short_input_file local short_output_file local output_temp_file local vbrfix_temp_path input_file="${1}" ## Build output file path based on input file path. input_file_path="$(dirname "${input_file}")" output_file_path="$(echo -n "${input_file_path}" | sed -e "s|^${input_path}|${output_path}|")" input_filename="$(basename "${input_file}")" output_filename="$(echo -n "${input_filename}" \ | sed -e "s/\.[^\.]*$/\.${output_extension}/")" input_file_mimetype="$(file --brief --mime-type "${input_file}")" input_file_format="$(mediainfo --Inform="Audio;%Format%" "${input_file}" | tr '[:lower:]' '[:upper:]')" # Detect input extension input_file_extension="$(echo -n "${input_filename}" \ | sed -e 's/^.*\.\([^\.]*\)$/\1/')" output_file="${output_file_path}/${output_filename}" #short_input_file="${short_path}/${input_filename}" short_input_file="${input_filename}" short_output_file="${output_filename}" # Generate a temporary filename for processing cache. # Use the same extension to make sure format detection work properly. output_temp_file="$(mktemp -t "tmp.XXXXXXXXXX")" mv "${output_temp_file}" "${output_temp_file}.${output_extension}" 2>&- output_temp_file="${output_temp_file}.${output_extension}" # Declare the variable for vbrfix temporary directory. vbrfix_temp_path='' #------------------------ # Trap handling function # use with: trap 'exit_convert_file_on_signal "${output_file}" "${output_temp_file}" "${vbrfix_temp_path}"' SIGHUP SIGINT SIGQUIT SIGTERM # # @param string $output_file The converted/copied output file. function exit_convert_file_on_signal() { local interrupt local output_file local output_temp_file local vbrfix_temp_path local output_filename interrupt=${?} output_file="${1}" output_temp_file="${2}" vbrfix_temp_path="${3}" output_filename="$(basename "${output_file}")" cecho 'boldred' "Conversion of '${output_filename}' interrupted." >&2 [[ -n "${output_file}" && -e "${output_file}" ]] && rm "${output_file}" [[ -n "${output_temp_file}" && -e "${output_temp_file}" ]] && rm "${output_temp_file}" [[ -n "${vbrfix_temp_path}" && -d "${vbrfix_temp_path}" ]] && rm -r "${vbrfix_temp_path}" local running_jobs=($(jobs -p)) #if [[ -n "${running_jobs}" ]]; then # cecho 'yellow' " -> Stopping conversion jobs (${running_jobs[@]})." >&2 # kill ${running_jobs[@]} 2>&- #fi if [[ ${interrupt} -eq 2 ]]; then # killing self if catched a SIGINT. # see http://mywiki.wooledge.org/SignalTrap trap - SIGINT kill -INT $$ else exit 0 fi } # exit_convert_file_on_signal() #------------------------ # Test if input file is an audio file. if [[ -z "${input_file_format}" ]]; then # Input file is not an audio file. echo -n ' - ' cecho 'magenta' -n "${short_input_file}" echo -n " is not a audio file... " # Update computed values. output_filename="${input_filename}" output_file="${output_file_path}/${output_filename}" short_output_file="${output_filename}" local copy_needed='FALSE' if [[ "${copy_all}" = 'True' ]]; then if [[ ! -e "${output_file}" ]]; then copy_needed='True' cecho 'blue' 'Copying.' else # Check if output file is older than input file. if [[ "${input_file}" -nt "${output_file}" ]]; then cecho 'blue' 'Updating.' copy_needed='True' else cecho 'yellow' 'Skipping.' fi fi else cecho 'yellow' 'Skipping.' fi if [[ "${copy_needed}" = 'True' ]]; then trap 'exit_convert_file_on_signal "${output_file}" "${vbrfix_temp_path}"' SIGHUP SIGINT SIGQUIT SIGTERM cp -a "${input_file}" "${output_file}" return $? fi return 0 fi local conversion_needed='False' # Check if output file already exists. if [[ "${input_extension}" = '*' \ || "${input_extension}" = "${input_file_extension}" ]]; then if [[ ! -e "${output_file}" ]]; then conversion_needed='True' else echo -n ' - ' cecho 'magenta' -n "${short_output_file}" echo -n " exists... " # Check if output file is older than input file. if [[ "${input_file}" -nt "${output_file}" ]]; then cecho 'blue' 'Updating.' conversion_needed='True' else cecho 'yellow' 'Skipping.' fi fi else # Found file has not the correct extension. cecho 'magenta' -n "${short_input_file}" echo " is not in source format... " cecho 'yellow' 'Skipping.' fi if [[ "${conversion_needed}" = 'True' ]]; then if [[ -e "${input_file}" ]]; then trap 'exit_convert_file_on_signal "${output_file}" "${output_temp_file}"' SIGHUP SIGINT SIGQUIT SIGTERM mkdir -p "${output_file_path}" if [[ -d "${output_file_path}" ]]; then # Remove output file if it already exists. if [[ -e "${output_file}" ]]; then rm "${output_file}" fi # TODO ! input_format is not defined here !! if [[ "${input_format}" = "${output_format}" ]]; then # Copy input to output (format is the same). # Convert input to output echo -n " - Copying " cecho 'magenta' -n "${short_input_file}" echo -n ' to ' cecho 'magenta' -n "${short_output_file}" echo -n '... ' cp "${input_file}" "${output_file}" cecho 'green' "Done." else # Convert input to output echo -n " - Converting " cecho 'magenta' -n "${short_input_file}" echo -n ' to ' cecho 'magenta' -n "${short_output_file}" echo -n '... ' local log_level='quiet' if [[ "${verbose}" = 'True' ]]; then log_level='debug' fi local encoding_options=() local pipe_mode='False' # Specific options for encoding mode. case "${encoding_mode}" in 'CBR' ) # Constant bitrate selected. encoding_options+=(-b:a "${encoding_bitrate}") ;; 'VBR' | * ) # Variable bitrate selected. Default. encoding_options+=(-q:a "${encoding_quality}") ;; esac local map_metadata=(-map_metadata 0:g) local metadata_json_address='tags' local output_options=() # Specific needs for some input formats/ case "${input_file_format}" in 'application/ogg' ) # Get input metadata from first audio stream and direct it to global. # See https://bugs.kde.org/show_bug.cgi?id=306895 map_metadata=(-map_metadata 0:s:0) # Clean picture metadata. output_options+=( -metadata "metadata_block_picture=''" ) ;; * ) # Do nothing. # map_metadata=(-map_metadata 0:g) ;; esac # Specific needs for some output formats case "${output_format}" in 'm4a' | 'aac' ) # avconv has an experimental aac encoder but aac-enc from libfdk-aac is better. # create the m4a aac file with aac-enc. decoding_options=() if type -f "fdkaac" &>'/dev/null'; then # Use fdkaac, the best command line for aac transcoding. # Specific options for encoding mode. case "${encoding_mode}" in 'CBR' ) # Constant bitrate selected. encoding_options=(--bitrate-mode 0 --bitrate "${encoding_bitrate}") ;; 'VBR' | * ) # Variable bitrate selected. Default. encoding_options=(--bitrate-mode "${encoding_quality}") ;; esac # Downsample > 96k sample rate FLAC files (192k). if [[ "${input_file_mimetype}" = 'audio/x-flac' ]]; then sample_rate="$(metaflac --show-sample-rate "${input_file}")" if [[ "${sample_rate}" -gt 96000 ]]; then decoding_options+=( '-ar' "$((sample_rate/2))" ) fi fi # Create temporary files. temporary_json_file="$(mktemp -t "tmp.XXXXXXXXXX.json")" # Fetch input file tags in json format. avprobe \ -loglevel "${log_level}" \ -show_format \ -show_streams \ -of 'json' \ "${input_file}" \ > "${temporary_json_file}" # normalise json file between ogg and others (keep only tags). # 1. Remove lines after brakets containing "TRACK" keyword: # trash trailing "tags" contents. sed -i -e '/"TRACK"/I,/\}/{/\}/Q}' "${temporary_json_file}" # 2. Remove everything up to the last line matching "tags": # Keep only metadata content. while grep -q '"tags"' "${temporary_json_file}"; do sed -i -e '0,/"tags"/d' "${temporary_json_file}" done translate_short_tags "${input_file_mimetype}" 'm4a' "${temporary_json_file}" # 3. Extract muscbrainz tags for specific inclusion as long tags. while IFS='' read -r tag || [[ -n "${tag}" ]]; do tag_name="$(echo "${tag}" | sed -e 's/^.*"\([^"]*\)"[ ]*:.*$/\1/')" tag_value="$(echo "${tag}" | sed -e 's/^.*"[^"]*"[ ]*:[ ]*"\([^"]*\)"*.*$/\1/')" # Translate MusicBrainz tags for M4A. translated_tag="$(translate_musicbrainz_for_m4a "${input_file_mimetype}" "${tag_name}")" if [[ -n "${translated_tag}" ]]; then encoding_options+=('--long-tag' "${translated_tag}:${tag_value}") fi done < "${temporary_json_file}" # 4. Add a new "tags": {} containing the metadata content. # Normalise the json file. sed -i -e ':a;N;$!ba;s/.*/{ "tags": { & } }/' "${temporary_json_file}" # Convert input file to aac. if [[ "${verbose}" = 'True' ]]; then avconv -i "${input_file}" \ -vn -sn "${decoding_options[@]}" \ -loglevel "${log_level}" \ -f caf pipe:1 \ | fdkaac \ --profile 2 \ --afterburner 1 \ --transport-format 0 \ --tag-from-json="${temporary_json_file}"?${metadata_json_address} \ "${encoding_options[@]}" \ - -o "${output_temp_file}" else avconv -i "${input_file}" \ -vn -sn "${decoding_options[@]}" \ -loglevel "${log_level}" \ -f caf pipe:1 \ | fdkaac \ --profile 2 \ --afterburner 1 \ --transport-format 0 \ --tag-from-json="${temporary_json_file}"?${metadata_json_address} \ "${encoding_options[@]}" \ - -o "${output_temp_file}" 2>'/dev/null' fi [[ -e "${temporary_json_file}" ]] && rm "${temporary_json_file}" else # Use aac-enc that is very disk intensive. # Create temporary files. temporary_wav_file="$(mktemp -t "tmp.XXXXXXXXXX")" temporary_m4a_file="$(mktemp -t "tmp.XXXXXXXXXX")" # Create temporary Wav file with avconv. avconv -y -i "${input_file}" \ -vn -sn \ -loglevel "${log_level}" \ -sample_fmt s16 -ac 2 -f wav \ "${temporary_wav_file}" if [[ -e "${temporary_wav_file}" ]]; then # flac -sdc "$i" | fdkaac - -o "${i[@]/%flac/m4a}" -p2 -m5 -a1 --tag-from-json="${i[@]/flac/json}"?format.tags # Specific options for encoding mode. case "${encoding_mode}" in 'CBR' ) # Constant bitrate selected. encoding_options=(-v 0 -r "${encoding_bitrate}") ;; 'VBR' | * ) # Variable bitrate selected. Default. encoding_options=(-v "${encoding_quality}") ;; esac # convert temporary wav file to m4a without metadata. aac-enc -t 2 "${encoding_options[@]}" "${temporary_wav_file}" "${temporary_m4a_file}" if [[ -e "${temporary_m4a_file}" ]]; then # copy metadata from input file to m4a file. avconv -y -i "${temporary_m4a_file}" \ -i "${input_file}" \ -vn -sn \ -loglevel "${log_level}" \ -map 0 -c:a copy -bsf:a aac_adtstoasc \ -map_metadata 1 -map_metadata 1:s:0 \ "${output_temp_file}" rm "${temporary_m4a_file}" fi # Remove temporary files. rm "${temporary_wav_file}" fi fi ;; * ) # Process formats mixing well with avconv. case "${output_format}" in 'flac' ) # No encoding options needed. encoding_options=() output_options=(${output_options[@]} -f "${output_format}") ;; 'ogg' | 'vorbis' ) # Set vorbis as default codec for ogg. output_options=(${output_options[@]} -codec:a 'libvorbis' -f 'ogg') # Map input metadata to all audio streams in ogg container. # See https://bugs.kde.org/show_bug.cgi?id=306895 if [[ "${input_file_mimetype}" = 'application/ogg' ]]; then map_metadata=(-map_metadata 0:s:0) else map_metadata=(-map_metadata:s:a 0:s:0) fi ;; 'm4a' | 'aac' ) # Set vorbis as default codec for ogg. output_options=(${output_options[@]} -ar 48000 -codec:a 'aac' -strict 'experimental') ;; 'mp3' ) # Force libmp3lame use. output_options=(${output_options[@]} -f "${output_format}" -codec:a 'libmp3lame') if [[ ${encoding_mode} = 'CBR' ]]; then pipe_mode='True' fi ;; * ) # Add output format. output_options=(${output_options[@]} -f "${output_format}") ;; esac if [[ "${pipe_mode}" = 'True' ]]; then # Fix for mp3 cbr format, breaks mp3 vbr files since # Xing header writing needs seekable file.. # Write output on pipe and then directed to file. # See: http://ffmpeg.zeranoe.com/forum/viewtopic.php?f=7&t=377 # # Note: log output is alway on 2. avconv keep 1 empty. avconv -i "${input_file}" \ -vn -sn \ "${map_metadata[@]}" \ -loglevel "${log_level}" \ "${output_options[@]}" \ "${encoding_options[@]}" \ - > "${output_temp_file}"; else avconv -y -i "${input_file}" \ -vn -sn \ "${map_metadata[@]}" \ -loglevel "${log_level}" \ "${output_options[@]}" \ "${encoding_options[@]}" \ "${output_temp_file}"; fi ;; esac if [[ ${?} -ne 0 ]]; then cecho 'red' "Failed." # Force > output to be written on disk. sync [[ -e "${output_file}" ]] && rm "${output_file}" [[ -e "${output_temp_file}" ]] && rm "${output_temp_file}" return 1 else # Force > output to be written on disk. sync cecho 'green' "Done." # Test if fix for MP3 VBR is needed. # See: http://ffmpeg.zeranoe.com/forum/viewtopic.php?f=7&t=377 if [[ "${output_format}" = 'mp3' && "${encoding_mode}" != 'CBR' ]]; then local output_file_audio_format="" local vbrfix_count=0 vbrfix_temp_path="$(mktemp -d -t "tmp.XXXXXXXXXX")" local vbrfix_output_file="${vbrfix_temp_path}/${output_filename}" while [[ "${output_file_audio_format}" != 'MPEG Audio' \ && "${output_file_audio_format}" != 'MPEG AUDIO' ]]; do # 5 tries maximum. if [[ ${vbrfix_count} -gt 5 ]]; then [[ -e "${output_temp_file}" ]] && rm "${output_temp_file}" [[ -e "${vbrfix_output_file}" ]] && rm "${vbrfix_output_file}" cecho 'red' -n "Failed." echo " Removing output file." return 1 break; fi if [[ ${vbrfix_count} -gt 0 ]]; then cecho 'red' "Failed." fi let vbrfix_count+=1 echo -n ' + Fixing ' cecho 'magenta' -n "${short_output_file}" echo -n " VBR header (try ${vbrfix_count})..." # Output file is MP3 and VBR. Apply header fix. # enter a temporary directory since vbrfix create log files in current directory (!) pushd "${vbrfix_temp_path}" >'/dev/null' if [[ "${verbose}" = 'True' ]]; then vbrfix "${output_temp_file}" "${vbrfix_output_file}" else vbrfix "${output_temp_file}" "${vbrfix_output_file}" &>'/dev/null' fi if [[ ! ${?} ]]; then # failure. Deleting output file by security. rm "${vbrfix_output_file}" output_file_audio_format='False' fi popd >'/dev/null' # Force output to be written on disk. # May not be necessary here. sync # Check that vbrfix has done his job correctly. [[ -e "${vbrfix_output_file}" ]] \ && output_file_audio_format="$(mediainfo --Inform="Audio;%Format%" "${vbrfix_output_file}")" done [[ -e "${vbrfix_output_file}" ]] && cecho 'green' "Done." # Use mp3val to cleanup vbrfix incorrect Xing header. echo -n ' + Cleaning up ' cecho 'magenta' -n "${short_output_file}" echo -n " VBR header..." if [[ "${verbose}" = 'True' ]]; then mp3val -f "${vbrfix_output_file}" else mp3val -f "${vbrfix_output_file}" &>'/dev/null' fi # Check mp3val result. if [[ ! ${?} ]]; then # failure. Deleting output file by security. rm "${vbrfix_output_file}" [[ -e "${output_file}" ]] && cecho 'red' "Failed." else mv "${vbrfix_output_file}" "${output_file}" 2>&- [[ -e "${output_file}" ]] && cecho 'green' "Done." fi # cleanup vbrfix temporary path. [[ -d "${vbrfix_temp_path}" ]] && rm -r "${vbrfix_temp_path}" else # Nothing to do but rename the file. mv "${output_temp_file}" "${output_file}" 2>&- fi # Delete temporary file if it is still present. [[ -e "${output_temp_file}" ]] && rm "${output_temp_file}" # Fetch cover art from input file. if [[ -e "${input_file}" && -e "${output_file}" ]]; then transfert_images "${input_file}" "${output_file}" fi fi fi else cecho 'red' "Error: unable to create folder '${output_file_path}'." >&2 return 1 fi else echo -n "'${input_file}' not found... " cecho 'yellow' 'Skipping.' fi fi return 0 } # convert_file() # Export convert_file for usage with Parallel / Sem. export -f 'convert_file' # Apply replaygain to a converted album. # # @param string $output_format The output audio format. # @param string $output_extension The output audio file extension. # @param string $verbose True for verbose output. # @param string $input_album_path The input album path. # @param string $threads The threads option for collectiongain. function replaygain_album() { [[ ${#} -ne 5 ]] && exit 1 local output_format local output_extension local verbose #local input_album_path local threads local output_album_path local album_name local album_files output_format="${1}" output_extension="${2}" verbose="${3}" export input_album_path="${4}" threads="${5}" if [[ "${threads}" = "100%" ]]; then threads=1 if [[ -e '/proc/cpuinfo' ]]; then threads=$(grep -c '^processor' '/proc/cpuinfo') fi fi ## Build output album path based on input album path. output_album_path="$(echo -n "${input_album_path}" \ | sed -e "s|^${input_path}|${output_path}|")" album_name="$(basename "${output_album_path}")" # test if output album path exists. [[ -d "${output_album_path}" ]] || return 1 # test if output album path is empty. shopt -s nullglob dotglob album_files=("${output_album_path}"/*".${output_extension}") shopt -u nullglob dotglob (( ${#album_files[*]} )) || return 1 echo -n " + Processing replay gain for album '" cecho 'magenta' -n "${album_name}" echo -n "'... " case "${output_format}" in 'flac' ) metaflac --add-replay-gain "${album_files[@]}" if [[ ! ${?} ]]; then cecho 'red' 'Failure.' return 1 fi ;; 'm4a' ) # Use aacgain instead of collectiongain. # collectiongain does not detect albums of m4a files. # reference (similar to mp3gain): # aacgain -a -t -p -s r -k -f local processed_list_log_path processed_list_log_path="$(mktemp -t "tmp.XXXXXXXXXX")" if [[ "${verbose}" = 'True' ]]; then aacgain -a -t -p -k -f -q "${album_files[@]}" | tee "${processed_list_log_path}" else aacgain -a -t -p -k -f -q "${album_files[@]}" > "${processed_list_log_path}" 2>&- fi local error_code=${?} if [[ ${error_code} -ne 0 ]]; then cecho 'red' 'Failure.' if [[ ${error_code} = 139 ]]; then # aacgain segfaulted. Handling the faulty aac file removal and regeneration. local faulty_file_index local faulty_file faulty_file_index=$(wc -l "${processed_list_log_path}" \ | cut --delimiter=' ' --fields=1) faulty_file="${album_files[${faulty_file_index}]}" if [[ -n "${faulty_file}" ]]; then local faulty_filename faulty_filename="$(basename "${faulty_file}")" cecho 'red' -n 'File ' cecho 'magenta' -n "${faulty_filename}" cecho 'red' -n ' is faulty... ' cecho 'red' -n 'Deleting... ' test -e "${faulty_file}" && rm "${faulty_file}" test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" cecho 'yellow' 'Regenerating.' return 139 fi fi test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" return 1 fi test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" ;; 'ogg' | 'vorbis' | 'mp3' | 'aac' | * ) if [[ "${verbose}" = 'True' ]]; then collectiongain --jobs="${threads}" "${output_album_path}" else collectiongain --jobs="${threads}" "${output_album_path}" > '/dev/null' fi if [[ ! ${?} ]]; then cecho 'red' 'Failure.' return 1 fi ;; esac # Old gain method. deprecated by python-rgain. # # case "${output_format}" in # 'mp3' ) # # reference: # # mp3gain -a -t -p -s r -k -f # local processed_list_log_path="$(mktemp -t "tmp.XXXXXXXXXX")" # if [[ "${verbose}" = 'True' ]]; then # mp3gain -a -t -p -k -f -q "${album_files[@]}" | tee "${processed_list_log_path}" # else # mp3gain -a -t -p -k -f -q "${album_files[@]}" > "${processed_list_log_path}" 2>&- # fi # local error_code=${?} # if [[ ${error_code} -ne 0 ]]; then # cecho 'red' 'Failure.' # if [[ ${error_code} = 139 ]]; then # # mp3gain segfaulted. Handling the faulty mp3 file removal and regeneration. # local faulty_file_index=$(wc -l "${processed_list_log_path}" \ # | cut --delimiter=' ' --fields=1) # local faulty_file="${album_files[${faulty_file_index}]}" # if [[ -n "${faulty_file}" ]]; then # local faulty_filename="$(basename "${faulty_file}")" # cecho 'red' -n 'File ' # cecho 'magenta' -n "${faulty_filename}" # cecho 'red' -n ' is faulty... ' # cecho 'red' -n 'Deleting... ' # test -e "${faulty_file}" && rm "${faulty_file}" # test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" # cecho 'yellow' 'Regenerating.' # return 139 # fi # fi # test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" # return 1 # fi # test -e "${processed_list_log_path}" && rm "${processed_list_log_path}" # ;; # 'flac' ) # metaflac --add-replay-gain "${album_files[@]}" # if [[ ! ${?} ]]; then # cecho 'red' 'Failure.' # return 1 # fi # ;; # 'ogg' | 'vorbis' ) # if [[ "${verbose}" = 'True' ]]; then # vorbisgain --album --fast "${album_files[@]}" # else # vorbisgain --album --fast --quiet "${album_files[@]}" # fi # if [[ ! ${?} ]]; then # cecho 'red' 'Failure.' # return 1 # fi # ;; # * ) # # unsupported format. # return 1 # ;; # esac cecho 'green' 'Done.' return 0 } #replaygain_album() export -f 'replaygain_album' # Delete files in output album when source is not found in input album. # # @param string $input_album_path The input album path. # @param string $input_path The input library path. # @param string $output_path The output library path. # @param string $lock_dir The mussync lock path. function sync_delete_album() { [[ ${#} -ne 4 ]] && exit 1 local input_album_path local input_path local output_path local lock_dir local output_album_path input_album_path="${1}" input_path="${2}" output_path="${3}" lock_dir="${4}" ## Build output album path based on input album path. output_album_path="$(echo -n "${input_album_path}" \ | sed -e "s|^${input_path}|${output_path}|")" if [[ -d "${output_album_path}" ]]; then find "${output_album_path}" -maxdepth 1 -type 'f' -print0 \ | grep --null-data -v "${lock_dir}" \ | while read -r -d '' 'output_file'; do local output_file_path local input_file_path local output_filename local input_filename_base local short_input_file_base local short_output_file ## Build input file path based on output file path. output_file_path="$(dirname "${output_file}")" input_file_path="$(echo -n "${output_file_path}" | sed -e "s|^${output_path}|${input_path}|")" output_filename="$(basename "${output_file}")" input_filename_base="$(echo -n "${output_filename}" | sed -e "s/\.[^\.]*$//")" short_input_file_base="${input_filename_base}.*" short_output_file="${output_filename}" # Test for file with same name (or without extension). if [[ ! -e "${input_file_path}/${output_filename}" ]]; then local input_file local short_input_file input_file="$(find "${input_file_path}" -maxdepth 1 -type 'f' -iname "${input_filename_base}.*")" short_input_file="$(basename "${input_file}")" if [[ -z "${input_file}" ]]; then # Input file not found. Removing output file. echo -n ' - Input file ' cecho 'magenta' -n "${short_input_file_base}" echo ' disappeared.' echo -n ' + Removing output file ' cecho 'magenta' -n "${short_output_file}" echo -n '... ' [[ -e "${output_file}" ]] && rm "${output_file}" if [[ -e "${output_file}" ]]; then cecho 'red' "Failed." else cecho 'green' "Done." fi fi fi done fi return 0 } # sync_delete_album() export -f 'sync_delete_album' # Convert an music album from input library to output library. # # @param string $threads The threads option for parallel. # @param string $replaygain True to apply album replay gain to converted files. # @param string $sync_delete True to delete files witch source is deleted from input album. # @param string $lock_dir The mussync lock path. # @param string $input_album_path The album path in input library. # # @return Exit with error if conversion failed. function convert_album() { [[ ${#} -ne 5 ]] && exit 1 # Enable the trapped errors to be passed to called functions. #set -o errtrace local threads local replaygain local sync_delete local lock_dir local input_album_path local album_name local short_album_path threads="${1}" replaygain="${2}" sync_delete="${3}" lock_dir="${4}" input_album_path="${5}" album_name="$(basename "${input_album_path}")" short_album_path="$(echo -n "${input_album_path}" | sed -e "s|^${input_path}/||")" #------------------------ # Trap handling function # # @param string $input_album_path The currently processed path. function exit_convert_album_on_signal() { local interrupt local input_album_path local album_name local running_jobs interrupt=${?} input_album_path="${1}" album_name="$(basename "${input_album_path}")" cecho 'boldred' "Convertion of album '${album_name}' interrupted." >&2 running_jobs=($(jobs -p)) if [[ ${#running_jobs[@]} -ne 0 ]]; then # cecho 'yellow' " -> Stopping conversion jobs (${running_jobs[@]})." >&2 kill "${running_jobs[@]}" 2>&- fi if [[ ${interrupt} -eq 2 ]]; then # killing self if catched a SIGINT. # see http://mywiki.wooledge.org/SignalTrap trap - SIGINT kill -INT $$ else exit 3 fi } # exitconvert_album__on_signal() #------------------------ trap 'exit_convert_album_on_signal "${input_album_path}"' SIGHUP SIGINT SIGQUIT SIGTERM # Find folder files and put them in a array. # See: http://mywiki.wooledge.org/BashFAQ/020 # local file_list_path="$(mktemp -t "tmp.XXXXXXXXXX")" # find "${input_album_path}" -maxdepth 1 -type 'f' -print0 \ # | sort --zero-terminated > "${file_list_path}" # unset files # declare -a files # while read -d '' -r; do # files+=("${REPLY}") # done < "${file_list_path}" declare -a files while read -d '' -r; do files+=("${REPLY}") done < <(find "${input_album_path}" -maxdepth 1 -type 'f' -print0 \ | sort --zero-terminated) # test -e "${file_list_path}" && rm "${file_list_path}" # see : http://mywiki.wooledge.org/UsingFind # Note to self: when -print0 option is not available. # (find "${input_album_path}" -maxdepth 1 -type 'f' -exec printf "%s\0" {} \;) # Test if folder has files. if [[ ${#files[@]} -gt 0 ]]; then cecho 'bold' -n ' * Processing folder ' cecho 'magentabold' -n "${short_album_path}" cecho 'bold' '...' # Delete files not matched in input library if option selected. # We do clean up before generating new files. if [[ "${sync_delete}" = 'True' ]]; then sync_delete_album "${input_album_path}" "${input_path}" "${output_path}" "${lock_dir}" fi # See http://mywiki.wooledge.org/ProcessManagement#I_want_to_process_a_bunch_of_files_in_parallel.2C_and_when_one_finishes.2C_I_want_to_start_the_next._And_I_want_to_make_sure_there_are_exactly_5_jobs_running_at_a_time. parallel --no-notice --halt 2 --jobs "${threads}" --null 'convert_file' ::: "${files[@]}" # Warning: when interrupted, parallel does not print threads output # (including the traps output). # use --res /path/to/folder to store and debug tasks outputs. # Wait for all files to be done. parallel --no-notice --wait # Compute album replaygain. if [[ "${replaygain}" = 'True' ]]; then replaygain_album "${output_format}" "${output_extension}" \ "${verbose}" "${input_album_path}" "${threads}" local return_code=${?} if [[ ${return_code} = 139 ]]; then # replaygain_album encountered a segfault. # Need another path to regenerated deleted faulty files. convert_album "${threads}" "${replaygain}" 'False' "${lock_dir}" "${input_album_path}" fi fi fi return 0 } # convert_album() export -f 'convert_album' ####################################################################################### ####################################################################################### ####################################################################################### # Include from http://wiki.grzegorz.wierzowiecki.pl/code:mutex-in-bash ####################################################################################### ####################################################################################### ####################################################################################### # Function called by trap. Perform exit cleanup # use to enable trap "exit_on_signal '${lock_path}'" SIGHUP SIGINT SIGQUIT SIGTERM # # @param string $lock_path The path to lock file. function exit_on_signal() { local interrupt="${?}" local lock_path="${1}" cecho 'boldred' "${script_name} is stopping at your request." >&2 local running_jobs=($(jobs -p)) if [[ ${#running_jobs[@]} -ne 0 ]]; then #cecho 'yellow' " -> Stopping conversion jobs (${running_jobs[@]})." >&2 kill "${running_jobs[@]}" 2>&- fi # remove lock file. [[ -n "${lock_path}" && -d "${lock_path}" ]] && rm -rf "${lock_path}" if [[ ${interrupt} -eq 2 ]]; then # killing self if catched a SIGINT. # see http://mywiki.wooledge.org/SignalTrap trap - SIGINT kill -INT $$ else exit 3 fi } # exit_on_signal() # lock dirs/files lock_dir=".${script_name}.lock" pid_file="${script_name}.pid" # exit codes and text for them - additional features nobody needs :-) # ENO_SUCCESS=0; ETXT[0]="ENO_SUCCESS" # ENO_GENERAL=1; ETXT[1]="ENO_GENERAL" # ENO_LOCKFAIL=2; ETXT[2]="ENO_LOCKFAIL" # ENO_RECVSIG=3; ETXT[3]="ENO_RECVSIG" base_lock_path="/var/lock" export lock_path="${base_lock_path}/${lock_dir}" # Lock the system. # # @param string $base_lock_path Optionnal custom path to lock file. # Default to /var/lock. # # @return unlock (rm) error code. function lock { if [[ -n "${1}" ]]; then base_lock_path="$(realpath_check "${1}")" fi lock_path="${base_lock_path}/${lock_dir}" local pid_path="${lock_path}/${pid_file}" if mkdir "${lock_path}" &>'/dev/null'; then # lock succeeded, store the PID echo "$$" >"${pid_path}" # the following handler will exit the script on receiving these signals # the trap on "0" (EXIT) from above will be triggered by this trap's # "exit" command! trap 'exit_on_signal "${lock_path}"' SIGHUP SIGINT SIGQUIT SIGTERM return ${ENO_SUCCESS} else # lock failed, now check if the other PID is alive other_pid="$(cat "${pid_path}" 2>&-)" # if cat wasn't able to read the file anymore, another instance probably is # about to remove the lock -- exit, we're *still* locked # Thanks to Grzegorz Wierzowiecki for pointing this race condition out on # http://wiki.grzegorz.wierzowiecki.pl/code:mutex-in-bash if [[ ${?} != 0 ]]; then # Pid file does not exists - probably directory is beeing deleted exit "${ENO_LOCKFAIL}" fi if ! kill -0 "${other_pid}" &>'/dev/null'; then # lock is stale, remove it and restart unlock lock "${base_lock_path}" return ${?} else # lock is valid and OTHERPID is active - exit, we're locked! cecho 'redbold' "lock failed, PID ${other_pid} is active" >&2 exit "${ENO_LOCKFAIL}" fi fi return 0 } # lock() # Unlock the system. # # @return unlock (rm) error code. function unlock { [[ -d "${lock_path}" ]] && rm -r "${lock_path}" &>'/dev/null' return $? } # unlock() ####################################################################################### ####################################################################################### ####################################################################################### # Include from /usr/share/doc/bash-doc/examples/functions/getoptx.bash of package bash-doc. ####################################################################################### ####################################################################################### ####################################################################################### function getoptex() { let $# || return 1 local optlist="${1#;}" let optind || optind=1 [[ $optind -lt $# ]] || return 1 shift $optind if [[ "$1" != "-" && "$1" != "${1#-}" ]]; then optind=$((optind+1)); if [[ "$1" != "--" ]]; then local o o="-${1#-$optofs}" for opt in ${optlist#;} do optopt="${opt%[;.:]}" unset optarg local opttype="${opt##*[^;:.]}" [[ -z "$opttype" ]] && opttype=";" if [[ ${#optopt} -gt 1 ]]; then # long-named option case $o in "--$optopt") if [[ "${opttype}" = ':' ]]; then optarg="${2}" if [[ -z "$optarg" ]]; then cecho 'redbold' "$0: error: $optopt must have an argument" >&2 optarg="${optopt}" optopt="?" return 1 fi optind=$((optind+1)) # skip option's argument elif [[ "${opttype}" = '.' ]]; then if [[ "${2}" != -* ]]; then optarg="${2}" optind=$((optind+1)) # skip option's argument fi fi return 0 ;; "--$optopt="*) if [[ "$opttype" = ";" ]]; then # error: must not have arguments let OPTERR && cecho 'redbold' "$0: error: $optopt must not have arguments" >&2 optarg="$optopt" optopt="?" return 1 fi optarg=${o#"--$optopt="} return 0 ;; esac else # short-named option case "$o" in "-$optopt") unset optofs if [[ "${opttype}" = ':' ]]; then optarg="${2}" if [[ -z "$optarg" ]]; then cecho 'redbold' "$0: error: -$optopt must have an argument" >&2 optarg="${optopt}" optopt="?" return 1 fi optind=$((optind+1)) # skip option's argument elif [[ "${opttype}" = '.' ]]; then if [[ "${2}" != -* ]]; then optarg="${2}" optind=$((optind+1)) # skip option's argument fi fi return 0 ;; "-$optopt"*) if [[ $opttype = ";" ]]; then # an option with no argument is in a chain of options optofs="$optofs?" # move to the next option in the chain optind=$((optind-1)) # the chain still has other options return 0 else unset optofs optarg="${o#-$optopt}" return 0 fi ;; esac fi done cecho 'redbold' "Error : invalid option : '${o}'." >&2 usage exit 1 fi fi optopt="?" unset optarg return 1 } function optlistex { local l="$1" local m # mask local r # to store result while [[ ${#m} -lt $((${#l}-1)) ]]; do m="$m?"; done # create a "???..." mask while [[ -n "$l" ]]; do r="${r:+"$r "}${l%$m}" # append the first character of $l to $r l="${l#?}" # cut the first charecter from $l m="${m#?}" # cut one "?" sign from m if [[ -n "${l%%[^:.;]*}" ]]; then # a special character (";", ".", or ":") was found r="$r${l%$m}" # append it to $r l="${l#?}" # cut the special character from l m="${m#?}" # cut one more "?" sign fi done echo "$r" } function getopt() { local optlist optlist=$(optlistex "$1") shift getoptex "$optlist" "$@" return $? } ####################################################################################### ####################################################################################### ####################################################################################### # Check for binaries presence check_binary "basename" "coreutils" > '/dev/null' check_binary "dirname" "coreutils" > '/dev/null' check_binary "mktemp" "mktemp" > '/dev/null' check_binary "sed" "sed" > '/dev/null' check_binary "bc" "bc" > '/dev/null' check_binary "avconv;ffmpeg" "libav-tools" > '/dev/null' check_binary "parallel" "parallel" > '/dev/null' check_binary "eyeD3;eyeD3-2.7;eyeD3-2.6" "eyed3" > '/dev/null' check_binary "mediainfo" "mediainfo" > '/dev/null' check_binary "vorbiscomment" "vorbis-tools" > '/dev/null' check_binary "metaflac" "flac" > '/dev/null' #check_binary "mp3gain;aacgain" "mp3gain" > '/dev/null' #check_binary "vorbisgain" "vorbisgain" > '/dev/null' check_binary "aacgain" "aacgain (ppa:flexiondotorg/audio)" > '/dev/null' check_binary "collectiongain" "python-rgain" > '/dev/null' check_binary "AtomicParsley" "atomicparsley" > '/dev/null' check_binary "vbrfix" "vbrfix" > '/dev/null' check_binary "mp3val" "mp3val" > '/dev/null' check_binary "fdkaac;aac-enc" "fdkaac aac-enc" > '/dev/null' # Application defaults # Export configuration for convert_file function. export input_path="" export output_path="" export input_format="*" export output_format="mp3" export encoding_mode="VBR" export encoding_bitrate="192k" export encoding_quality="5" replaygain="false" threads="100%" export sync_delete='False' export copy_all='False' export quiet="False" export verbose="False" export input_extension="${input_format}" export output_extension="${output_format}" # Parse options using getoptex from /usr/share/doc/bash-doc/examples/functions/getoptx.bash while getoptex "help h input-path: in: i: output-path: out: o: input-format: if: output-format: of: e: bitrate. b. quality. q. threads: t: replaygain gain g sync-delete delete d copy-all copy c quiet silent s verbose v" "${@}"; do # Options debuging. # echo "Option <$optopt> ${optarg:+has an arg <$optarg>}" case "${optopt}" in 'input-path' | 'in' | 'i' ) input_path="$(realpath_check "${optarg}")" if [[ -z "${output_path}" ]]; then output_path="${input_path}" fi ;; 'output-path' | 'out' | 'o' ) output_path="$(realpath_check "${optarg}")" ;; 'input-format' | 'if' ) if [[ -n "${optarg}" ]]; then input_format="${optarg}" fi ;; 'output-format' | 'of' | 'e' ) if [[ -n "${optarg}" ]]; then output_format="${optarg}" fi ;; 'bitrate' | 'b' ) encoding_mode="CBR" if [[ -n "${optarg}" ]]; then encoding_bitrate="${optarg}" fi ;; 'quality' | 'q' ) encoding_mode="VBR" if [[ -n "${optarg}" ]]; then encoding_quality="${optarg}" fi ;; 'replaygain' | 'gain' | 'g' ) replaygain='True' ;; 'threads' | 't' ) if [[ -n "${optarg}" ]]; then threads="${optarg}" fi ;; 'sync-delete' | 'delete' | 'd' ) sync_delete='True' ;; 'copy-all' | 'copy' | 'c' ) copy_all='True' ;; 'quiet' | 'silent' | 's' ) quiet='True' ;; 'verbose' | 'v' ) verbose='True' ;; 'help' | 'h' | * ) usage 0 ;; esac done shift $((optind-1)) if [[ -z "${input_path}" ]]; then cecho 'redbold' 'Error: no input path provided.' >&2 usage 1 fi if [[ "${verbose}" = 'True' ]]; then quiet='False' fi # Render formats with lowercase. input_format="$(tr '[:upper:]' '[:lower:]' <<< "${input_format}")" output_format="$(tr '[:upper:]' '[:lower:]' <<< "${output_format}")" # TODO : implement a less basic extension detection based on format. export input_extension="${input_format}" case "${input_format}" in 'vorbis' | 'ogg' ) input_format='vorbis' input_extension='ogg' ;; 'aac' | 'm4a' ) input_format='aac' input_extension='m4a' ;; esac export output_extension="${output_format}" case "${output_format}" in 'flac' | 'aiff' ) # Defaults are good. ;; 'mp3' ) # Invert quality setting (9 is lowest, 0 highest). # This way aac, mp3 and ogg/vorbis quality is set the same way. encoding_quality=$((9 - encoding_quality)) ;; 'ogg' | 'vorbis' ) output_extension='ogg' output_format='vorbis' ;; 'aac' | 'm4a' ) output_extension='m4a' output_format='aac' # Change quality setting range (1 is lowest, 5 highest, 0 is CBR). # This way aac, mp3 and ogg/vorbis quality is set the same way. encoding_quality="$(bc <<< "scale=10; result = 1.5 + (4 * (${encoding_quality} / 9)); scale=0; result / 1")" ;; * ) echo "Error: unsupported output format '${output_format}'." >&2 exit 1 esac ## Lock output path for this script. lock "${output_path}" ############################### # # Convert input files to output format. # ################################ # Enable the trapped errors to be passed to called functions. #set -o errtrace # Process albums (aka. folders) in input library. if [[ "${quiet}" = 'True' ]]; then find "${input_path}" -type 'd' \ -exec bash -c 'convert_album "${@}"' _ "${threads}" "${replaygain}" "${sync_delete}" "${lock_dir}" {} \; >'/dev/null' else find "${input_path}" -type 'd' \ -exec bash -c 'convert_album "${@}"' _ "${threads}" "${replaygain}" "${sync_delete}" "${lock_dir}" {} \; fi unlock exit 0