diff --git a/.gitignore b/.gitignore index bc3dd24..ec72114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ ACTIVATION .authcode *aax +*jpg +*json Audiobook/* diff --git a/AAXtoMP3 b/AAXtoMP3 index e729761..0a6c2e0 100755 --- a/AAXtoMP3 +++ b/AAXtoMP3 @@ -9,14 +9,14 @@ usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level ] [--no-clobber] [--target_dir ] [--complete_dir ] [--validate] [--loglevel ] [--keep-author ] [--author ] [--{dir,file,chapter}-naming-scheme ] -[--continue ] {FILES}\n' +[--use-audible-cli-data] [--audible-cli-library-file ] [--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 +dirNameScheme= # Custom directory naming scheme, default is $genre/$artist/$title customDNS=0 fileNameScheme= # Custom file naming scheme, default is $title customFNS=0 @@ -31,6 +31,13 @@ continue=0 # Default off, If set Transcoding is skipped and cha 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 +ffmpegPath= # Set a custom path, useful for using the updated version that supports aaxc +ffmpegName=ffmpeg # Set a custom ffmpeg binary name, useful tailoring to local setup +ffprobeName=ffprobe # Set a custom ffprobe binary name, useful tailoring to local setup +library_file= # Libraryfile generated by mkb79/audible-cli # ----- # Code tip Do not have any script above this point that calls a function or a binary. If you do @@ -41,8 +48,6 @@ 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. @@ -52,7 +57,7 @@ while true; do # 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 ;; + -e:m4a | -a | --aac ) 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. @@ -77,12 +82,22 @@ while true; do -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 ;; + # Path of the library-file, generated by mkb79/audible-cli (audible library export -o ./library.tsv) + -L | --audible-cli-library-file ) library_file="$2"; shift 2 ;; # Compression level --level ) level="$2"; shift 2 ;; # Keep author number n --keep-author ) keepArtist="$2"; shift 2 ;; # Author override --author ) authorOverride="$2"; shift 2 ;; + # Ffmpeg path override + --ffmpeg-path ) ffmpegPath="$2"; shift 2 ;; + # Ffmpeg name override + --ffmpeg-name ) ffmpegName="$2"; shift 2 ;; + # Ffprobe name override + --ffprobe-name ) ffprobeName="$2"; shift 2 ;; # Command synopsis. -h | --help ) printf "$usage" $0 ; exit ;; # Standard flag signifying the end of command line processing. @@ -192,14 +207,34 @@ 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 +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 + +# Use custom ffmpeg (and ffprobe) binary ( --ffmpeg-path flag) +if [ -n "$ffmpegPath" ]; then + FFMPEG="$ffmpegPath/${ffmpegName}" + FFPROBE="$ffmpegPath/${ffprobeName}" +else + FFMPEG="${ffmpegName}" + FFPROBE="${ffprobeName}" +fi + +debug_vars "ffmpeg/ffprobe paths" FFMPEG FFPROBE + # ----- # Detect which annoying version of grep we have -GREP=$(grep --version | grep -q GNU && echo "grep" || echo "ggrep") if ! [[ $(type -P "$GREP") ]]; then echo "$GREP (GNU grep) is not in your PATH" echo "Without it, this script will break." @@ -207,9 +242,17 @@ if ! [[ $(type -P "$GREP") ]]; then 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 -SED=$(sed --version 2>&1 | $GREP -q GNU && echo "sed" || echo "gsed") if ! [[ $(type -P "$SED") ]]; then echo "$SED (GNU sed) is not in your PATH" echo "Without it, this script will break." @@ -219,7 +262,7 @@ fi # ----- # Detect ffmpeg and ffprobe -if [[ "x$(type -P ffmpeg)" == "x" ]]; then +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:" @@ -232,7 +275,7 @@ fi # ----- # Detect ffmpeg and ffprobe -if [[ "x$(type -P ffprobe)" == "x" ]]; then +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:" @@ -290,12 +333,6 @@ if [ -z $auth_code ]; then auth_code=`head -1 ~/.authcode` fi fi -# No point going on if no authcode found. -if [ -z $auth_code ]; then - echo "ERROR Missing authcode" - echo "$usage" - exit 1 -fi # ----- # Check the target dir for if set if it is writable @@ -384,8 +421,8 @@ validate_aax() { # 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. - output="$(ffprobe -loglevel warning -activation_bytes ${auth_code} -i "${media_file}" 2>&1)" + # 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 @@ -398,7 +435,7 @@ validate_aax() { # This is a big test only performed when the --validate switch is passed. if [[ "${VALIDATE}" == "1" ]]; then - output="$(ffmpeg -hide_banner -activation_bytes ${auth_code} -i "${media_file}" -vn -f null - 2>&1)" + output="$("$FFMPEG" -hide_banner ${decrypt_param} -i "${media_file}" -vn -f null - 2>&1)" if [[ $? != "0" ]] ; then log "ERROR: Invalid File: ${media_file}" else @@ -413,12 +450,71 @@ validate_aax() { 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 + + # Test for library file + if [[ ! -r "${library_file}" ]] ; then + library_file_exists=0 + debug "library file not found" + else + library_file_exists=1 + debug "library file found" + 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" + "$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() @@ -432,6 +528,51 @@ save_metadata() { 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 ':' and '/' 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}" \ + | $SED 's@[:/]@@g' >> "$metadata_file" + # In case we want to use a single file m4b we need to extract the + # chapter titles from the .json generated by audible–cli and store + # them correctly formatted for mp4chaps in a chapter.txt + if [ "${mode}" == "single" ]; then + # Creating a temp file to store the chapter data collected in save_metadata, as the output + # folder will only be defined after save_metadata has been executed. + # This file is only required when using audible-cli data and executing in single mode to + # get proper chapter titles in single file m4b output. + tmp_chapter_file="${working_directory}/chapter.txt" + jq -r \ + 'def pad(n): tostring | if (n > length) then ((n - length) * "0") + . else . end; + .content_metadata.chapter_info.chapters | + reduce .[] as $c ([]; if $c.chapters? then .+[$c | del(.chapters)]+[$c.chapters] else .+[$c] end) | flatten | + to_entries | + .[] | + "CHAPTER\((.key))=\((((((.value.start_offset_ms / (1000*60*60)) /24 | floor) *24 ) + ((.value.start_offset_ms / (1000*60*60)) %24 | floor)) | pad(2))):\(((.value.start_offset_ms / (1000*60)) %60 | floor | pad(2))):\(((.value.start_offset_ms / 1000) %60 | floor | pad(2))).\((.value.start_offset_ms % 1000 | pad(3))) +CHAPTER\((.key))NAME=\(.value.title)"' "${extra_chapter_file}" > "${tmp_chapter_file}" + fi + + # get extra meta data from library.tsv + if [[ "${library_file_exists}" == 1 ]]; then + asin=$(jq -r '.content_metadata.content_reference.asin' "${extra_chapter_file}") + if [[ ! -z "${asin}" ]]; then + lib_entry=$($GREP "^${asin}" "${library_file}") + if [[ ! -z "${lib_entry}" ]]; then + series_title=$(echo "${lib_entry}" | awk -F '\t' '{print $6}') + series_sequence=$(echo "${lib_entry}" | awk -F '\t' '{print $7}') + $SED -i "/^ Metadata:/a\\ + series : ${series_title}\\ + series_sequence : ${series_sequence}" "${metadata_file}" + fi + fi + fi + fi debug "Metadata file $metadata_file" debug_file "$metadata_file" } @@ -453,15 +594,59 @@ 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. @@ -488,12 +673,14 @@ do album_artist="$(get_metadata_value album_artist)" fi fi - title=$(get_metadata_value title | $SED 's/'\:'/'-'/g' | $SED 's/- /-/g' | xargs -0) - title=${title:0:100} + 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)" + series="$(get_metadata_value series)" + series_sequence="$(get_metadata_value series_sequence)" # Get more tags with mediainfo if [[ $(type -P mediainfo) ]]; then @@ -516,9 +703,9 @@ do # 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}/" + output_directory="${targetdir}/${currentDirNameScheme}" else - output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}/" + output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}" fi # Define the output_file @@ -531,8 +718,9 @@ do 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 + log "Noclobber enabled but directory '${output_directory}' exists. Skipping to avoid overwriting" + rm -f "${metadata_file}" "${tmp_chapter_file}" + continue fi mkdir -p "${output_directory}" @@ -546,11 +734,12 @@ do # 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 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 + 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 -activation_bytes "${auth_code}" -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${aax_file} | cut -d . -f 1)" + 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))" @@ -569,10 +758,10 @@ do 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 -activation_bytes "${auth_code}" -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 @@ -598,18 +789,38 @@ do # ----- fi # Grab the cover art if available. - cover_file="${output_directory}/cover.jpg" + cover_file="${output_directory}/${currentFileNameScheme}.jpg" if [ "${continue}" == "0" ]; then - if [ "$((${loglevel} > 1))" == "1" ]; then - log "Extracting cover into ${cover_file}..." + 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}" + else + # Audible-cli not used, extract the cover from the aax file + if [ "$((${loglevel} > 1))" == "1" ]; then + log "Extracting cover into ${cover_file}..." + fi + &1 | $GREP -Po "[0-9]+(?=x[0-9]+)" | tail -n 1) + if (( ${cover_width} % 2 == 1 )); then + if [ "$((${loglevel} > 1))" == "1" ]; then + log "Cover ${cover_file} has odd width ${cover_width}, setting extra_crop_cover to make even." + fi + # 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. + # Set the flag only if we use a cover art with an odd width. + extra_crop_cover='-vf crop=trunc(iw/2)*2:trunc(ih/2)*2' + fi + # ----- - # OK now spit the file if that's what you want. - # If we want multiple file we take the big mp3 and split it by chapter. - # Not all audio encodings make sense with multiple chapter outputs. See options section - # for more detail + # 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" @@ -684,34 +895,35 @@ do #ffmpeg version 4+ and on the output for all older versions. split_input="" split_output="" - if [ "$(($(ffmpeg -version | head -1 | cut -d \ -f 3 | cut -d . -f 1) > 3))" = "1" ]; then + 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 chapterNameScheme chapter_title chapter_file + 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 | 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}/${fileNameScheme}.chapters.txt" + if [ "${audibleCli}" == "1" ]; then + mv "${tmp_chapter_file}" "${output_directory}/${currentFileNameScheme}.chapters.txt" + else + "$FFPROBE" -i "${aax_file}" -print_format csv -show_chapters 2>/dev/null | awk -F "," '{printf "CHAPTER%d=%02d:%02d:%02.3f\nCHAPTER%dNAME=%s\n", NR, $5/60/60, $5/60%60, $5%60, NR, $8}' > "${output_directory}/${currentFileNameScheme}.chapters.txt" + fi + $SED -i 's/\,000/\.000/' "${output_directory}/${currentFileNameScheme}.chapters.txt" mp4chaps -i "${output_file}" fi fi + if [ -f "${cover_file}" ]; then + log "Adding cover art" + # FFMPEG does not support MPEG-4 containers fully # + if [ "${container}" == "mp4" ] ; then + mp4art --add "${cover_file}" "${output_file}" + # FFMPEG for everything else # + else + # Create temporary output file name - ensure extention matches previous appropriate output file to keep ffmpeg happy + cover_output_file="${output_file%.*}.cover.${output_file##*.}" + # Copy audio stream from current output, and video stream from cover file, setting appropriate metadata + 0))" == "1" ]; then diff --git a/README.md b/README.md index 3c319a4..ee45eda 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # AAXtoMP3 -The purpose of this software is to convert AAX files to common MP3, M4A, M4B, flac and ogg formats +The purpose of this software is to convert AAX (or AAXC) files to common MP3, M4A, M4B, flac and ogg formats through a basic bash script frontend to FFMPEG. -Audible uses this file format to maintain DRM restrictions on their audio +Audible uses the AAX file format to maintain DRM restrictions on their audio books and if you download your book through your library it will be stored in this format. @@ -13,33 +13,32 @@ create a method for you to download and store your books just in case Audible fails for some reason. ## Requirements -* bash 4.3.42 or later tested -* ffmpeg version 2.8.3 or later -* libmp3lame (came from lame package on Arch, not sure where else this is stored) -* grep Some OS distributions do not have it installed. -* sed Some OS versions will need to install gnu sed. +* bash 3.2.57 or later tested +* ffmpeg version 2.8.3 or later (4.4 or later if the input file is `.aaxc`) +* libmp3lame - (typically 'lame' in your system's package manager) +* GNU grep - macOS or BSD users may need to install through package manager +* GNU sed - see above +* GNU find - see above +* jq - only if `--use-audible-cli-data` is set or if converting an .aaxc file * mp4art used to add cover art to m4a and m4b files. Optional * mediainfo used to add additional media tags like narrator. Optional -## OSX -Thanks to thibaudcolas, this script has been tested on OSX 10.11.6 El Capitan. YMMV, but it should work for -conversions in OSX. It is recommended that you install GNU grep using 'brew install grep' for chapter padding to work. - -## AUR -Thanks to kbabioch, this script has also been packaged in the [AUR](https://aur.archlinux.org/packages/aaxtomp3-git/). Note that you will still need to extract your activation bytes before use. - ## Usage(s) ``` -bash AAXtoMP3 [-f|--flac] [-o|--opus] [-a|-aac] [-s|--single] [--level ] [-c|--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [-A|--authcode ] [-n|--no-clobber] [-t|--target_dir ] [-C|--complete_dir ] [-V|--validate] [-d|--debug] [-h|--help] [--continue ] ... +bash AAXtoMP3 [-f|--flac] [-o|--opus] [-a|--aac] [-s|--single] [--level ] [-c|--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [-A|--authcode ] [-n|--no-clobber] [-t|--target_dir ] [-C|--complete_dir ] [-V|--validate] [--use-audible-cli-data]] [-d|--debug] [-h|--help] [--continue ] ... +``` +or if you want to get guided through the options +``` +bash interactiveAAXtoMP3 [-a|--advanced] [-h|--help] ``` * **<AAX INPUT_FILES>**... are considered input file(s), useful for batching! -## Options +## Options for AAXtoMP3 * **-f** or **--flac** Flac Encoding and as default produces a single file. * **-o** or **--opus** Ogg/Opus Encoding defaults to multiple file output by chapter. The extension is .ogg * **-a** or **--aac** AAC Encoding and produce a m4a single files output. -* **-A** or **--authcode <AUTHCODE>** for this execution of the command use the provided <AUTHCODE> to decode the AAX file. +* **-A** or **--authcode <AUTHCODE>** for this execution of the command use the provided <AUTHCODE> to decode the AAX file. Not needed if the source file is .aaxc. * **-n** or **--no-clobber** If set and the target directory already exists, AAXtoMP3 will exit without overwriting anything. * **-t** or **--target_dir <PATH>** change the default output location to the named <PATH>. Note the default location is ./Audiobook of the directory to which each AAX file resides. * **-C** or **--complete_dir <PATH>** a directory to place aax files after they have been decoded successfully. Note make a back up of your aax files prior to using this option. Just in case something goes wrong. @@ -57,23 +56,33 @@ bash AAXtoMP3 [-f|--flac] [-o|--opus] [-a|-aac] [-s|--single] [--level &1 | $GREP -q GNU && echo "sed" || echo "gsed") +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 + +# ===Get options from last time=================================================================================================================== + +# ===Set default values=== +lastcodec="mp3" +lastcompression="4" +lastchapters="yes" +lastauthcode="" +lastloglevel="1" + +# ===Get Values from last time=== +if [ -f ".interactivesave" ]; then + for ((i=1;i<=$(wc -l .interactivesave | cut -d " " -f 1);i++)) do + line=$(head -$i .interactivesave | tail -1) + case $(echo $line | cut -d " " -f 1 | $SED 's/.$//') in + codec ) lastcodec="$(echo $line | cut -d " " -f 2)";; + compression ) lastcompression="$(echo $line | cut -d " " -f 2)";; + chapters ) lastchapters="$(echo $line | cut -d " " -f 2)";; + authcode ) lastauthcode="$(echo $line | cut -d " " -f 2)";; + loglevel ) lastloglevel="$(echo $line | cut -d " " -f 2)";; + * ) rm .interactivesave; exit 1;; + esac + done +fi + +# ===Get options for AAXtoMP3===================================================================================================================== + +# ===Codec=== +while true; do + clear; + read -e -p "codec (mp3/m4a/m4b/flac/aac/opus): " -i "$lastcodec" codec + case "$codec" in + mp3 ) summary="$summary""codec: $codec"; call="$call -e:mp3"; break;; + m4a ) summary="$summary""codec: $codec"; call="$call -e:m4a"; break;; + m4b ) summary="$summary""codec: $codec"; call="$call -e:m4b"; break;; + flac ) summary="$summary""codec: $codec"; call="$call --flac"; break;; + aac ) summary="$summary""codec: $codec"; call="$call --aac"; break;; + opus ) summary="$summary""codec: $codec"; call="$call --opus"; break;; + esac +done + +# ===Compression=== +while true; do + clear; echo -e "$summary" + case "$codec" in + mp3 ) maxlevel=9;; + flac ) maxlevel=12;; + opus ) maxlevel=10;; + * ) break;; + esac + read -e -p "compression level (0-$maxlevel): " -i "$lastcompression" compression + if [[ $compression =~ ^[0-9]+$ ]] && [[ "$compression" -ge "0" ]] && [[ "$compression" -le "$maxlevel" ]]; then + summary="$summary""\ncompression level: $compression" + call="$call --level $compression" + break + fi +done + +# ===Chapters=== +while true; do + clear; echo -e "$summary" + read -e -p "chapters (yes/no/chapternumber to continue with): " -i "$lastchapters" chapters + case "$chapters" in + ^[0-9]+$ ) summary="$summary""\nchapters: $chapters"; call="$call -c --continue ${chapters}"; break;; + yes ) summary="$summary""\nchapters: $chapters"; call="$call -c"; break;; + no ) summary="$summary""\nchapters: $chapters"; call="$call -s"; break;; + esac +done + +# ===Authcode=== +if ! [ -r .authcode ] || [ -r ~/.authcode ]; then + clear; echo -e "$summary" + read -e -p "Authcode: " -i "$lastauthcode" authcode + summary="$summary""\nauthcode: $authcode" + call="$call -A $authcode" +fi + +# ===Loglevel=== +while true; do + clear; echo -e "$summary" + read -e -p "loglevel (0/1/2/3): " -i "$lastloglevel" loglevel + if [[ $loglevel =~ ^[0-9]+$ ]] && [[ "$loglevel" -ge "0" ]] && [[ "$loglevel" -le "3" ]]; then + summary="$summary""\nloglevel: $loglevel" + call="$call -l $loglevel" + break + fi +done + +# ===File=== +clear; echo -e "$summary" +read -p "aax-file: " file +file="${file%\'}" #remove suffix ' if file is given via drag'n'drop +file="${file#\'}" #remove prefix ' if file is given via drag'n'drop +savefile="$summary" +summary="$summary""\naax-file: $file" +call="$call $(echo $file | $SED "s;~;$HOME;")" + +# ===Summerize chosen options and call AAXtoMP3=================================================================================================== + +# ===Summary=== +clear; echo -e "$summary\n" +echo -e "$call\n" + +# ===Save chosen options=== +echo -e $savefile | $SED "s;\ level:;:;" > .interactivesave + +# ===Call AAXtoMP3=== +$call