diff options
| author | Craig Jennings <c@cjennings.net> | 2025-05-21 22:01:35 -0500 | 
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-05-21 22:01:35 -0500 | 
| commit | b4463015b97912658d630377fafbf630f7588d1e (patch) | |
| tree | d04b66d992fe2ce88391889c21c5d8dc97acd0ef /dotfiles/system/.local/bin/AAXtoMP3 | |
| parent | 548154ea395356868e87980b149dfc0abdc84e17 (diff) | |
moving arch dotfiles into archsetup
Diffstat (limited to 'dotfiles/system/.local/bin/AAXtoMP3')
| -rwxr-xr-x | dotfiles/system/.local/bin/AAXtoMP3 | 908 | 
1 files changed, 908 insertions, 0 deletions
diff --git a/dotfiles/system/.local/bin/AAXtoMP3 b/dotfiles/system/.local/bin/AAXtoMP3 new file mode 100755 index 0000000..adc91ef --- /dev/null +++ b/dotfiles/system/.local/bin/AAXtoMP3 @@ -0,0 +1,908 @@ +#!/usr/bin/env bash + + +# ======================================================================== +# Command Line Options + +# Usage Synopsis. +usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level <COMPRESSIONLEVEL>] +[--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [--authcode <AUTHCODE>] [--no-clobber] +[--target_dir <PATH>] [--complete_dir <PATH>] [--validate] [--loglevel <LOGLEVEL>] +[--keep-author <N>] [--author <AUTHOR>] [--{dir,file,chapter}-naming-scheme <STRING>]   +[--use-audible-cli-data] [--continue <CHAPTERNUMBER>] {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 "<full path to file>" +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)  <blanks> 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}"' +    </dev/null ffmpeg -loglevel error \ +      -stats \ +      ${decrypt_param} \ +      -i "${aax_file}" \ +      -vn \ +      -codec:a "${codec}" \ +      ${compression_level_param} \ +      -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}" \ +      -metadata description="${description}" \ +      -metadata composer="${narrator}" \ +      -metadata publisher="${publisher}" \ +      -f ${container} \ +      "${output_file}" +    if [ "$((${loglevel} > 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 +      </dev/null ffmpeg -loglevel error -activation_bytes "${auth_code}" -i "${aax_file}" -an -codec:v copy "${cover_file}" +    fi +  fi + +  # ----- +  # If mode=chaptered, split the big converted file by chapter and remove it afterwards. +  # Not all audio encodings make sense with multiple chapter outputs (see options section) +  if [ "${mode}" == "chaptered" ]; then +    # Playlist m3u support +    playlist_file="${output_directory}/${currentFileNameScheme}.m3u" +    if [ "${continue}" == "0" ]; then +      if [ "$((${loglevel} > 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 +          </dev/null ffmpeg -loglevel quiet \ +            -nostats \ +            ${split_input} \ +            -i "${output_file}" \ +            -i "${cover_file}" \ +            ${extra_crop_cover} \ +            ${split_output} \ +            -map 0:0 \ +            -map 1:0 \ +            -acodec ${chapter_codec} \ +            -metadata:s:v title="Album cover" \ +            -metadata:s:v comment="Cover (Front)" \ +            -metadata track="${chapternum}" \ +            -metadata title="${chapter}" \ +            -metadata:s:a title="${chapter}" \ +            -metadata:s:a track="${chapternum}" \ +            -map_chapters -1 \ +            -f ${container} \ +            "${chapter_file}" +          # ----- +          if [ "$((${loglevel} < 2))" == "1" ]; then +            progressbar ${chapternum} ${chaptercount} +          fi +          # OK lets get what need for the next chapter in the Playlist m3u file. +          # Playlist creation. +          duration=$(echo "${chapter_end} - ${chapter_start%?}" | bc) +          echo "#EXTINF:${duration%.*},${title} - ${chapter}" >>  "${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  | 
