#!/usr/bin/env bash # ======================================================================== # Command Line Options # Usage Synopsis. usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level ] [--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [--authcode ] [--no-clobber] [--target_dir ] [--complete_dir ] [--validate] [--loglevel ] [--keep-author ] [--author ] [--{dir,file,chapter}-naming-scheme ] [--use-audible-cli-data] [--continue ] {FILES}\n' codec=libmp3lame # Default encoder. extension=mp3 # Default encoder extension. level=-1 # Compression level. Can be given for mp3, flac and opus. -1 = default/not specified. mode=chaptered # Multi file output auth_code= # Required to be set via file or option. targetdir= # Optional output location. Note default is basedir of AAX file. dirNameScheme= # Custom directory naming scheme, default is $genre/$author/$title customDNS=0 fileNameScheme= # Custom file naming scheme, default is $title customFNS=0 chapterNameScheme= # Custom chapter naming scheme, default is '$title-$(printf %0${#chaptercount}d $chapternum) $chapter' (BookTitle-01 Chapter 1) customCNS=0 completedir= # Optional location to move aax files once the decoding is complete. container=mp3 # Just in case we need to change the container. Used for M4A to M4B VALIDATE=0 # Validate the input aax file(s) only. No Transcoding of files will occur loglevel=1 # Loglevel: 0: Show progress only; 1: default; 2: a little more information, timestamps; 3: debug noclobber=0 # Default off, clobber only if flag is enabled continue=0 # Default off, If set Transcoding is skipped and chapter splitting starts at chapter continueAt. continueAt=1 # Optional chapter to continue splitting the chapters. keepArtist=-1 # Default off, if set change author metadata to use the passed argument as field authorOverride= # Override the author, ignoring the metadata audibleCli=0 # Default off, Use additional data gathered from mkb79/audible-cli aaxc_key= # Initialize variables, in case we need them in debug_vars aaxc_iv= # Initialize variables, in case we need them in debug_vars # ----- # Code tip Do not have any script above this point that calls a function or a binary. If you do # the $1 will no longer be a ARGV element. So you should only do basic variable setting above here. # # Process the command line options. This allows for un-ordered options. Sorta like a getops style while true; do case "$1" in # Flac encoding -f | --flac ) codec=flac; extension=flac; mode=single; container=flac; shift ;; # Apple m4a music format. -a | --aac ) codec=copy; extension=m4a; mode=single; container=m4a; shift ;; # Ogg Format -o | --opus ) codec=libopus; extension=opus; container=ogg; shift ;; # If appropriate use only a single file output. -s | --single ) mode=single; shift ;; # If appropriate use only a single file output. -c | --chaptered ) mode=chaptered; shift ;; # This is the same as --single option. -e:mp3 ) codec=libmp3lame; extension=mp3; mode=single; container=mp3; shift ;; # Identical to --acc option. -e:m4a ) codec=copy; extension=m4a; mode=single; container=mp4; shift ;; # Similar to --aac but specific to audio books -e:m4b ) codec=copy; extension=m4b; mode=single; container=mp4; shift ;; # Change the working dir from AAX directory to what you choose. -t | --target_dir ) targetdir="$2"; shift 2 ;; # Use a custom directory naming scheme, with variables. -D | --dir-naming-scheme ) dirNameScheme="$2"; customDNS=1; shift 2 ;; # Use a custom file naming scheme, with variables. -F | --file-naming-scheme ) fileNameScheme="$2"; customFNS=1; shift 2 ;; # Use a custom chapter naming scheme, with variables. --chapter-naming-scheme ) chapterNameScheme="$2"; customCNS=1; shift 2 ;; # Move the AAX file to a new directory when decoding is complete. -C | --complete_dir ) completedir="$2"; shift 2 ;; # Authorization code associate with the AAX file(s) -A | --authcode ) auth_code="$2"; shift 2 ;; # Don't overwrite the target directory if it already exists -n | --no-clobber ) noclobber=1; shift ;; # Extremely verbose output. -d | --debug ) loglevel=3; shift ;; # Set loglevel. -l | --loglevel ) loglevel="$2"; shift 2 ;; # Validate ONLY the aax file(s) No transcoding occurs -V | --validate ) VALIDATE=1; shift ;; # continue splitting chapters at chapter continueAt --continue ) continueAt="$2"; continue=1; shift 2 ;; # Use additional data got with mkb79/audible-cli --use-audible-cli-data ) audibleCli=1; shift ;; # Compression level --level ) level="$2"; shift 2 ;; # Keep author number n --keep-author ) keepArtist="$2"; shift 2 ;; # Author override --author ) authorOverride="$2"; shift 2 ;; # Command synopsis. -h | --help ) printf "$usage" $0 ; exit ;; # Standard flag signifying the end of command line processing. -- ) shift; break ;; # Anything else stops command line processing. * ) break ;; esac done # ----- # Empty argv means we have nothing to do so lets bark some help. if [ "$#" -eq 0 ]; then printf "$usage" $0 exit 1 fi # Setup safer bash script defaults. set -o errexit -o noclobber -o nounset -o pipefail # ======================================================================== # Utility Functions # ----- # debug # debug "Some longish message" debug() { if [ $loglevel == 3 ] ; then echo "$(date "+%F %T%z") DEBUG ${1}" fi } # ----- # debug dump contents of a file to STDOUT # debug "" debug_file() { if [ $loglevel == 3 ] ; then echo "$(date "+%F %T%z") DEBUG" echo "=Start==========================================================================" cat "${1}" echo "=End============================================================================" fi } # ----- # debug dump a list of internal script variables to STDOUT # debug_vars "Some Message" var1 var2 var3 var4 var5 debug_vars() { if [ $loglevel == 3 ] ; then msg="$1"; shift ; # Grab the message args=("$@") # Grab the rest of the args # determine the length of the longest key l=0 for (( n=0; n<${#args[@]}; n++ )) ; do (( "${#args[$n]}" > "$l" )) && l=${#args[$n]} done # Print the Debug Message echo "$(date "+%F %T%z") DEBUG ${msg}" echo "=Start==========================================================================" # Using the max length of a var name we dynamically create the format. fmt="%-"${l}"s = %s\n" for (( n=0; n<${#args[@]}; n++ )) ; do eval val="\$${args[$n]}" ; # We save off the value of the var in question for ease of coding. echo "$(printf "${fmt}" ${args[$n]} "${val}" )" done echo "=End============================================================================" fi } # ----- # log log() { if [ "$((${loglevel} > 1))" == "1" ] ; then echo "$(date "+%F %T%z") ${1}" else echo "${1}" fi } # ----- #progressbar produces a progressbar in the style of # process: |####### | XX% (part/total unit) # which is gonna be overwritten by the next line. progressbar() { #get input part=${1} total=${2} #compute percentage and make print_percentage the same length regardless of the number of digits. percentage=$((part*100/total)) if [ "$((percentage<10))" = "1" ]; then print_percentage=" $percentage" elif [ "$((percentage<100))" = "1" ]; then print_percentage=" $percentage" else print_percentage="$percentage"; fi #draw progressbar with one # for every 5% and blank spaces for the missing part. progressbar="" for (( n=0; n<(percentage/5); n++ )) ; do progressbar="$progressbar#"; done for (( n=0; n<(20-(percentage/5)); n++ )) ; do progressbar="$progressbar "; done #print progressbar echo -ne "Chapter splitting: |$progressbar| $print_percentage% ($part/$total chapters)\r" } # Print out what we have already after command line processing. debug_vars "Command line options as set" codec extension mode container targetdir completedir auth_code keepArtist authorOverride audibleCli # ======================================================================== # Variable validation if [ $(uname) = 'Linux' ]; then GREP="grep" FIND="find" SED="sed" else GREP="ggrep" FIND="gfind" SED="gsed" fi # ----- # Detect which annoying version of grep we have if ! [[ $(type -P "$GREP") ]]; then echo "$GREP (GNU grep) is not in your PATH" echo "Without it, this script will break." echo "On macOS, you may want to try: brew install grep" exit 1 fi # ----- # Detect which annoying version of find we have if ! [[ $(type -P "$FIND") ]]; then echo "$FIND (GNU find) is not in your PATH" echo "Without it, this script will break." echo "On macOS, you may want to try: brew install findutils" exit 1 fi # ----- # Detect which annoying version of sed we have if ! [[ $(type -P "$SED") ]]; then echo "$SED (GNU sed) is not in your PATH" echo "Without it, this script will break." echo "On macOS, you may want to try: brew install gnu-sed" exit 1 fi # ----- # Detect ffmpeg and ffprobe if [[ "x$(type -P ffmpeg)" == "x" ]]; then echo "ERROR ffmpeg was not found on your env PATH variable" echo "Without it, this script will break." echo "INSTALL:" echo "MacOS: brew install ffmpeg" echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc" echo "Ubuntu (20.04): sudo apt-get update; sudo apt-get install ffmpeg x264 x265 bc" echo "RHEL: yum install ffmpeg" exit 1 fi # ----- # Detect ffmpeg and ffprobe if [[ "x$(type -P ffprobe)" == "x" ]]; then echo "ERROR ffprobe was not found on your env PATH variable" echo "Without it, this script will break." echo "INSTALL:" echo "MacOS: brew install ffmpeg" echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc" echo "RHEL: yum install ffmpeg" exit 1 fi # ----- # Detect if we need mp4art for cover additions to m4a & m4b files. if [[ "x${container}" == "xmp4" && "x$(type -P mp4art)" == "x" ]]; then echo "WARN mp4art was not found on your env PATH variable" echo "Without it, this script will not be able to add cover art to" echo "m4b files. Note if there are no other errors the AAXtoMP3 will" echo "continue. However no cover art will be added to the output." echo "INSTALL:" echo "MacOS: brew install mp4v2" echo "Ubuntu: sudo apt-get install mp4v2-utils" fi # ----- # Detect if we need mp4chaps for adding chapters to m4a & m4b files. if [[ "x${container}" == "xmp4" && "x$(type -P mp4chaps)" == "x" ]]; then echo "WARN mp4chaps was not found on your env PATH variable" echo "Without it, this script will not be able to add chapters to" echo "m4a/b files. Note if there are no other errors the AAXtoMP3 will" echo "continue. However no chapter data will be added to the output." echo "INSTALL:" echo "MacOS: brew install mp4v2" echo "Ubuntu: sudo apt-get install mp4v2-utils" fi # ----- # Detect if we need mediainfo for adding description and narrator if [[ "x$(type -P mediainfo)" == "x" ]]; then echo "WARN mediainfo was not found on your env PATH variable" echo "Without it, this script will not be able to add the narrator" echo "and description tags. Note if there are no other errors the AAXtoMP3" echo "will continue. However no such tags will be added to the output." echo "INSTALL:" echo "MacOS: brew install mediainfo" echo "Ubuntu: sudo apt-get install mediainfo" fi # ----- # Obtain the authcode from either the command line, local directory or home directory. # See Readme.md for details on how to acquire your personal authcode for your personal # audible AAX files. if [ -z $auth_code ]; then if [ -r .authcode ]; then auth_code=`head -1 .authcode` elif [ -r ~/.authcode ]; then auth_code=`head -1 ~/.authcode` fi fi # ----- # Check the target dir for if set if it is writable if [[ "x${targetdir}" != "x" ]]; then if [[ ! -w "${targetdir}" || ! -d "${targetdir}" ]] ; then echo "ERROR Target Directory does not exist or is not writable: \"$targetdir\"" echo "$usage" exit 1 fi fi # ----- # Check the completed dir for if set if it is writable if [[ "x${completedir}" != "x" ]]; then if [[ ! -w "${completedir}" || ! -d "${completedir}" ]] ; then echo "ERROR Complete Directory does not exist or is not writable: \"$completedir\"" echo "$usage" exit 1 fi fi # ----- # Check whether the loglevel is valid if [ "$((${loglevel} < 0 || ${loglevel} > 3 ))" = "1" ]; then echo "ERROR loglevel has to be in the range from 0 to 3!" echo " 0: Show progress only" echo " 1: default" echo " 2: a little more information, timestamps" echo " 3: debug" echo "$usage" exit 1 fi # ----- # If a compression level is given, check whether the given codec supports compression level specifiers and whether the level is valid. if [ "${level}" != "-1" ]; then if [ "${codec}" == "flac" ]; then if [ "$((${level} < 0 || ${level} > 12 ))" = "1" ]; then echo "ERROR Flac compression level has to be in the range from 0 to 12!" echo "$usage" exit 1 fi elif [ "${codec}" == "libopus" ]; then if [ "$((${level} < 0 || ${level} > 10 ))" = "1" ]; then echo "ERROR Opus compression level has to be in the range from 0 to 10!" echo "$usage" exit 1 fi elif [ "${codec}" == "libmp3lame" ]; then if [ "$((${level} < 0 || ${level} > 9 ))" = "1" ]; then echo "ERROR MP3 compression level has to be in the range from 0 to 9!" echo "$usage" exit 1 fi else echo "ERROR This codec doesnt support compression levels!" echo "$usage" exit 1 fi fi # ----- # Clean up if someone hits ^c or the script exits for any reason. trap 'rm -r -f "${working_directory}"' EXIT # ----- # Set up some basic working files ASAP. Note the trap will clean this up no matter what. working_directory=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` metadata_file="${working_directory}/metadata.txt" # ----- # Validate the AAX and extract the metadata associated with the file. validate_aax() { local media_file media_file="$1" # Test for existence if [[ ! -r "${media_file}" ]] ; then log "ERROR File NOT Found: ${media_file}" return 1 else if [[ "${VALIDATE}" == "1" ]]; then log "Test 1 SUCCESS: ${media_file}" fi fi # Clear the errexit value we want to capture the output of the ffprobe below. set +e errexit # Take a look at the aax file and see if it is valid. If the source file is aaxc, we give ffprobe additional flags output="$(ffprobe -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)" # If invalid then say something. if [[ $? != "0" ]] ; then # No matter what lets bark that something is wrong. log "ERROR: Invalid File: ${media_file}" elif [[ "${VALIDATE}" == "1" ]]; then # If the validate option is present then lets at least state what is valid. log "Test 2 SUCCESS: ${media_file}" fi # This is a big test only performed when the --validate switch is passed. if [[ "${VALIDATE}" == "1" ]]; then output="$(ffmpeg -hide_banner ${decrypt_param} -i "${media_file}" -vn -f null - 2>&1)" if [[ $? != "0" ]] ; then log "ERROR: Invalid File: ${media_file}" else log "Test 3 SUCCESS: ${media_file}" fi fi # Dump the output of the ffprobe command. debug "$output" # Turn it back on. ffprobe is done. set -e errexit } validate_extra_files() { local extra_media_file extra_find_command extra_media_file="$1" # Bash trick to delete, non greedy, from the end up until the first '-' extra_title="${extra_media_file%-*}" # Using this is not ideal, because if the naming scheme is changed then # this part of the script will break # AAX file: BookTitle-LC_128_44100_stereo.aax # Cover file: BookTitle_(1215).jpg # Chapter file: BookTitle-chapters.json # Chapter extra_chapter_file="${extra_title}-chapters.json" # Cover extra_dirname="$(dirname "${extra_media_file}")" extra_find_command='$FIND "${extra_dirname}" -maxdepth 1 -regex ".*/${extra_title##*/}_([0-9]+)\.jpg"' # We want the output of the find command, we will turn errexit on later set +e errexit extra_cover_file="$(eval ${extra_find_command})" extra_eval_comm="$(eval echo ${extra_find_command})" set -e errexit if [[ "${aaxc}" == "1" ]]; then # bash trick to get file w\o extention (delete from end to the first '.') extra_voucher="${extra_media_file%.*}.voucher" if [[ ! -r "${extra_voucher}" ]] ; then log "ERROR File NOT Found: ${extra_voucher}" return 1 fi aaxc_key=$(jq -r '.content_license.license_response.key' "${extra_voucher}") aaxc_iv=$(jq -r '.content_license.license_response.iv' "${extra_voucher}") fi debug_vars "Audible-cli variables" extra_media_file extra_title extra_chapter_file extra_cover_file extra_find_command extra_eval_comm extra_dirname extra_voucher aaxc_key aaxc_iv # Test for chapter file existence if [[ ! -r "${extra_chapter_file}" ]] ; then log "ERROR File NOT Found: ${extra_chapter_file}" return 1 fi if [[ "x${extra_cover_file}" == "x" ]] ; then log "ERROR Cover File NOT Found" return 1 fi debug "All expected audible-cli related file are here" } # ----- # Inspect the AAX and extract the metadata associated with the file. save_metadata() { local media_file media_file="$1" ffprobe -i "$media_file" 2> "$metadata_file" if [[ $(type -P mediainfo) ]]; then echo "Mediainfo data START" >> "$metadata_file" # Mediainfo output is structured like ffprobe, so we append it to the metadata file and then parse it with get_metadata_value() # mediainfo "$media_file" >> "$metadata_file" # Or we only get the data we are intrested in: # Description echo "Track_More :" "$(mediainfo --Inform="General;%Track_More%" "$media_file")" >> "$metadata_file" # Narrator echo "nrt :" "$(mediainfo --Inform="General;%nrt%" "$media_file")" >> "$metadata_file" # Publisher echo "pub :" "$(mediainfo --Inform="General;%pub%" "$media_file")" >> "$metadata_file" echo "Mediainfo data END" >> "$metadata_file" fi if [[ "${audibleCli}" == "1" ]]; then # If we use data we got with audible-cli, we delete conflicting chapter infos $SED -i '/^ Chapter #/d' "${metadata_file}" # Some magic: we parse the .json generated by audible-cli. # to get the output structure like the one generated by ffprobe, # we use some characters (#) as placeholder, add some new lines, # put a ',' after the start value, we calculate the end of each chapter # as start+length, and we convert (divide) the time stamps from ms to s. # Then we delete all ':' since they make a filename invalid. jq -r '.content_metadata.chapter_info.chapters[] | "Chapter # start: \(.start_offset_ms/1000), end: \((.start_offset_ms+.length_ms)/1000) \n#\n# Title: \(.title)"' "${extra_chapter_file}" \ | tr -d ':' >> "$metadata_file" fi debug "Metadata file $metadata_file" debug_file "$metadata_file" } # ----- # Reach into the meta data and extract a specific value. # This is a long pipe of transforms. # This finds the first occurrence of the key : value pair. get_metadata_value() { local key key="$1" # Find the key in the meta data file # Extract field value # Remove the following /'s "(Unabridged) at start end and multiples. echo "$($GREP --max-count=1 --only-matching "${key} *: .*" "$metadata_file" | cut -d : -f 2- | $SED -e 's#/##g;s/ (Unabridged)//;s/^[[:blank:]]\+//g;s/[[:blank:]]\+$//g' | $SED 's/[[:blank:]]\+/ /g')" } # ----- # specific variant of get_metadata_value bitrate is important for transcoding. get_bitrate() { get_metadata_value bitrate | $GREP --only-matching '[0-9]\+' } # Save the original value, since in the for loop we overwrite # $audibleCli in case the file is aaxc. If the file is the # old aax, reset the variable to be the one passed by the user originalAudibleCliVar=$audibleCli # ======================================================================== # Main Transcode Loop for aax_file do # If the file is in aaxc format, set the proper variables if [[ ${aax_file##*.} == "aaxc" ]]; then # File is the new .aaxc aaxc=1 audibleCli=1 else # File is the old .aax aaxc=0 # If some previous file in the loop are aaxc, the $audibleCli variable has been overwritten, so we reset it to the original one audibleCli=$originalAudibleCliVar fi debug_vars "Variables set based on file extention" aaxc originalAudibleCliVar audibleCli # No point going on if no authcode found and the file is aax. # If we use aaxc as input, we do not need it # if the string $auth_code is null and the format is not aaxc; quit. We need the authcode if [ -z $auth_code ] && [ "${aaxc}" = "0" ]; then echo "ERROR Missing authcode, can't decode $aax_file" echo "$usage" exit 1 fi # Validate the input aax file. Note this happens no matter what. # It's just that if the validate option is set then we skip to next file. # If however validate is not set and we proceed with the script any errors will # case the script to stop. # If the input file is aaxc, we need to first get the audible_key and audible_iv # We get them in the function validate_extra_files if [[ ${audibleCli} == "1" ]] ; then # If we have additional files (obtained via audible-cli), be sure that they # exists and they are in the correct location. validate_extra_files "${aax_file}" fi # Set the needed params to decrypt the file. Needed in all command that require ffprobe or ffmpeg # After validate_extra_files, since the -audible_key and -audible_iv are read in that function if [[ ${aaxc} == "1" ]] ; then decrypt_param="-audible_key ${aaxc_key} -audible_iv ${aaxc_iv}" else decrypt_param="-activation_bytes ${auth_code}" fi validate_aax "${aax_file}" if [[ ${VALIDATE} == "1" ]] ; then # Don't bother doing anything else with this file. continue fi # ----- # Make sure everything is a variable. Simplifying Command interpretation save_metadata "${aax_file}" genre=$(get_metadata_value genre) if [ "x${authorOverride}" != "x" ]; then #Manual Override artist="${authorOverride}" album_artist="${authorOverride}" else if [ "${keepArtist}" != "-1" ]; then # Choose artist from the one that are present in the metadata. Comma separated list of names # remove leading space; 'C. S. Lewis' -> 'C.S. Lewis' artist="$(get_metadata_value artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')" album_artist="$(get_metadata_value album_artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')" else # The default artist=$(get_metadata_value artist) album_artist="$(get_metadata_value album_artist)" fi fi title=$(get_metadata_value title) title=${title:0:128} bitrate="$(get_bitrate)k" album="$(get_metadata_value album)" album_date="$(get_metadata_value date)" copyright="$(get_metadata_value copyright)" # Get more tags with mediainfo if [[ $(type -P mediainfo) ]]; then narrator="$(get_metadata_value nrt)" description="$(get_metadata_value Track_More)" publisher="$(get_metadata_value pub)" else narrator="" description="" publisher="" fi # Define the output_directory if [ "${customDNS}" == "1" ]; then currentDirNameScheme="$(eval echo "${dirNameScheme}")" else # The Default currentDirNameScheme="${genre}/${artist}/${title}" fi # If we defined a target directory, use it. Otherwise use the location of the AAX file if [ "x${targetdir}" != "x" ] ; then output_directory="${targetdir}/${currentDirNameScheme}/" else output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}/" fi # Define the output_file if [ "${customFNS}" == "1" ]; then currentFileNameScheme="$(eval echo "${fileNameScheme}")" else # The Default currentFileNameScheme="${title}" fi output_file="${output_directory}/${currentFileNameScheme}.${extension}" if [[ "${noclobber}" = "1" ]] && [[ -d "${output_directory}" ]]; then log "Noclobber enabled but directory '${output_directory}' exists. Exiting to avoid overwriting" exit 0 fi mkdir -p "${output_directory}" if [ "$((${loglevel} > 0))" = "1" ]; then # Fancy declaration of which book we are decoding. Including the AUTHCODE. dashline="----------------------------------------------------" log "$(printf '\n----Decoding---%s%s--%s--' "${title}" "${dashline:${#title}}" "${auth_code}")" log "Source: ${aax_file}" fi # Big long DEBUG output. Fully describes the settings used for transcoding. # Note this is a long debug command. It's not critical to operation. It's purely for people debugging # and coders wanting to extend the script. debug_vars "Book and Variable values" title auth_code aaxc aaxc_key aaxc_iv mode aax_file container codec bitrate artist album_artist album album_date genre copyright narrator description publisher currentDirNameScheme output_directory currentFileNameScheme output_file metadata_file working_directory # Display the total length of the audiobook in format hh:mm:ss # 10#$var force base-10 interpretation. By default it's base-8, so values like 08 or 09 are not octal numbers total_length="$(ffprobe -v error ${decrypt_param} -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${aax_file}" | cut -d . -f 1)" hours="$((total_length/3600))" if [ "$((hours<10))" = "1" ]; then hours="0$hours"; fi minutes="$((total_length/60-60*10#$hours))" if [ "$((minutes<10))" = "1" ]; then minutes="0$minutes"; fi seconds="$((total_length-3600*10#$hours-60*10#$minutes))" if [ "$((seconds<10))" = "1" ]; then seconds="0$seconds"; fi log "Total length: $hours:$minutes:$seconds" # If level != -1 specify a compression level in ffmpeg. compression_level_param="" if [ "${level}" != "-1" ]; then compression_level_param="-compression_level ${level}" fi # ----- if [ "${continue}" == "0" ]; then # This is the main work horse command. This is the primary transcoder. # This is the primary transcode. All the heavy lifting is here. debug 'ffmpeg -loglevel error -stats ${decrypt_param} -i "${aax_file}" -vn -codec:a "${codec}" -ab ${bitrate} -map_metadata -1 -metadata title="${title}" -metadata artist="${artist}" -metadata album_artist="${album_artist}" -metadata album="${album}" -metadata date="${album_date}" -metadata track="1/1" -metadata genre="${genre}" -metadata copyright="${copyright}" "${output_file}"' 0))" == "1" ]; then log "Created ${output_file}." fi # ----- fi # Grab the cover art if available. cover_file="${output_directory}/cover.jpg" extra_crop_cover='' if [ "${continue}" == "0" ]; then if [ "${audibleCli}" == "1" ]; then # We have a better quality cover file, copy it. if [ "$((${loglevel} > 1))" == "1" ]; then log "Copy cover file to ${cover_file}..." fi cp "${extra_cover_file}" "${cover_file}" # We now set a variable, ${extra_crop_cover}, which contains an additional # ffmpeg flag. It crops the cover so the width and the height is divisible by two. # Since the standard (in the aax file) image resolution is 512, we set the flag # only if we use a custom cover art. extra_crop_cover='-vf crop=trunc(iw/2)*2:trunc(ih/2)*2' else # Audible-cli not used, extract the cover from the aax file if [ "$((${loglevel} > 1))" == "1" ]; then log "Extracting cover into ${cover_file}..." fi 0))" == "1" ]; then log "Creating PlayList ${currentFileNameScheme}.m3u" fi echo '#EXTM3U' > "${playlist_file}" fi # Determine the number of chapters. chaptercount=$($GREP -Pc "Chapter.*start.*end" $metadata_file) if [ "$((${loglevel} > 0))" == "1" ]; then log "Extracting ${chaptercount} chapter files from ${output_file}..." if [ "${continue}" == "1" ]; then log "Continuing at chapter ${continueAt}:" fi fi chapternum=1 #start progressbar for loglevel 0 and 1 if [ "$((${loglevel} < 2))" == "1" ]; then progressbar 0 ${chaptercount} fi # We pipe the metadata_file in read. # Example of the section that we are interested in: # # Chapter #0:0: start 0.000000, end 1928.231474 # Metadata: # title : Chapter 1 # # Then read the line in these variables: # first Chapter # _ #0:0: # _ start # chapter_start 0.000000, # _ end # chapter_end 1928.231474 while read -r -u9 first _ _ chapter_start _ chapter_end do # Do things only if the line starts with 'Chapter' if [[ "${first}" = "Chapter" ]]; then # The next line (Metadata:...) gets discarded read -r -u9 _ # From the line 'title : Chapter 1' we save the third field and those after in chapter read -r -u9 _ _ chapter # The formatting of the chapters names and the file names. # Chapter names are used in a few place. # Define the chapter_file if [ "${customCNS}" == "1" ]; then chapter_title="$(eval echo "${chapterNameScheme}")" else # The Default chapter_title="${title}-$(printf %0${#chaptercount}d $chapternum) ${chapter}" fi chapter_file="${output_directory}/${chapter_title}.${extension}" # Since the .aax file allready got converted we can use # -acodec copy, which is much faster than a reencodation. # Since there is an issue when using copy on flac, where # the duration of the chapters gets shown as if they where # as long as the whole audiobook. chapter_codec="" if test "${extension}" = "flac"; then chapter_codec="flac "${compression_level_param}"" else chapter_codec="copy" fi #Since there seems to be a bug in some older versions of ffmpeg, which makes, that -ss and -to #have to be apllied to the output file, this makes, that -ss and -to get applied on the input for #ffmpeg version 4+ and on the output for all older versions. split_input="" split_output="" if [ "$(($(ffmpeg -version | $SED -E 's/[^0-9]*([0-9]).*/\1/g;1q') > 3))" = "1" ]; then split_input="-ss ${chapter_start%?} -to ${chapter_end}" else split_output="-ss ${chapter_start%?} -to ${chapter_end}" fi # Big Long chapter debug debug_vars "Chapter Variables:" cover_file chapter_start chapter_end chapternum chapter chapterNameScheme chapter_title chapter_file if [ "$((${continueAt} > ${chapternum}))" = "0" ]; then # Extract chapter by time stamps start and finish of chapter. # This extracts based on time stamps start and end. if [ "$((${loglevel} > 1))" == "1" ]; then log "Splitting chapter ${chapternum}/${chaptercount} start:${chapter_start%?}(s) end:${chapter_end}(s)" fi > "${playlist_file}" echo "${chapter_title}.${extension}" >> "${playlist_file}" fi chapternum=$((chapternum + 1 )) fi done 9< "$metadata_file" # Clean up of working directory stuff. rm "${output_file}" if [ "$((${loglevel} > 1))" == "1" ]; then log "Done creating chapters for ${output_directory}." else #ending progress bar echo "" fi else # Perform file tasks on output file. # ---- # ffmpeg seems to copy only chapter position, not chapter names. if [[ ${container} == "mp4" && $(type -P mp4chaps) ]]; then ffprobe -i "${aax_file}" -print_format csv -show_chapters 2>/dev/null | awk -F "," '{printf "CHAPTER%02d=%02d:%02d:%02.3f\nCHAPTER%02dNAME=%s\n", NR, $5/60/60, $5/60%60, $5%60, NR, $8}' > "${output_directory}/${currentFileNameScheme}.chapters.txt" mp4chaps -i "${output_file}" fi fi # ----- # Announce that we have completed the transcode if [ "$((${loglevel} > 0))" == "1" ]; then log "Complete ${title}" fi # Lastly get rid of any extra stuff. rm "${metadata_file}" # Move the aax file if the decode is completed and the --complete_dir is set to a valid location. # Check the target dir for if set if it is writable if [[ "x${completedir}" != "x" ]]; then if [ "$((${loglevel} > 0))" == "1" ]; then log "Moving Transcoded ${aax_file} to ${completedir}" fi mv "${aax_file}" "${completedir}" fi done