diff options
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 |
