├── .gitignore ├── AAXtoMP3 ├── LICENSE ├── README.md ├── _config.yml └── interactiveAAXtoMP3 /.gitignore: -------------------------------------------------------------------------------- 1 | ACTIVATION 2 | .authcode 3 | *aax 4 | *jpg 5 | *json 6 | Audiobook/* 7 | -------------------------------------------------------------------------------- /AAXtoMP3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | # ======================================================================== 5 | # Command Line Options 6 | 7 | # Usage Synopsis. 8 | usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level ] 9 | [--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [--authcode ] [--no-clobber] 10 | [--target_dir ] [--complete_dir ] [--validate] [--loglevel ] 11 | [--keep-author ] [--author ] [--{dir,file,chapter}-naming-scheme ] 12 | [--use-audible-cli-data] [--audible-cli-library-file ] [--continue ] {FILES}\n' 13 | codec=libmp3lame # Default encoder. 14 | extension=mp3 # Default encoder extension. 15 | level=-1 # Compression level. Can be given for mp3, flac and opus. -1 = default/not specified. 16 | mode=chaptered # Multi file output 17 | auth_code= # Required to be set via file or option. 18 | targetdir= # Optional output location. Note default is basedir of AAX file. 19 | dirNameScheme= # Custom directory naming scheme, default is $genre/$artist/$title 20 | customDNS=0 21 | fileNameScheme= # Custom file naming scheme, default is $title 22 | customFNS=0 23 | chapterNameScheme= # Custom chapter naming scheme, default is '$title-$(printf %0${#chaptercount}d $chapternum) $chapter' (BookTitle-01 Chapter 1) 24 | customCNS=0 25 | completedir= # Optional location to move aax files once the decoding is complete. 26 | container=mp3 # Just in case we need to change the container. Used for M4A to M4B 27 | VALIDATE=0 # Validate the input aax file(s) only. No Transcoding of files will occur 28 | loglevel=1 # Loglevel: 0: Show progress only; 1: default; 2: a little more information, timestamps; 3: debug 29 | noclobber=0 # Default off, clobber only if flag is enabled 30 | continue=0 # Default off, If set Transcoding is skipped and chapter splitting starts at chapter continueAt. 31 | continueAt=1 # Optional chapter to continue splitting the chapters. 32 | keepArtist=-1 # Default off, if set change author metadata to use the passed argument as field 33 | authorOverride= # Override the author, ignoring the metadata 34 | audibleCli=0 # Default off, Use additional data gathered from mkb79/audible-cli 35 | aaxc_key= # Initialize variables, in case we need them in debug_vars 36 | aaxc_iv= # Initialize variables, in case we need them in debug_vars 37 | ffmpegPath= # Set a custom path, useful for using the updated version that supports aaxc 38 | ffmpegName=ffmpeg # Set a custom ffmpeg binary name, useful tailoring to local setup 39 | ffprobeName=ffprobe # Set a custom ffprobe binary name, useful tailoring to local setup 40 | library_file= # Libraryfile generated by mkb79/audible-cli 41 | 42 | # ----- 43 | # Code tip Do not have any script above this point that calls a function or a binary. If you do 44 | # the $1 will no longer be a ARGV element. So you should only do basic variable setting above here. 45 | # 46 | # Process the command line options. This allows for un-ordered options. Sorta like a getops style 47 | while true; do 48 | case "$1" in 49 | # Flac encoding 50 | -f | --flac ) codec=flac; extension=flac; mode=single; container=flac; shift ;; 51 | # Ogg Format 52 | -o | --opus ) codec=libopus; extension=opus; container=ogg; shift ;; 53 | # If appropriate use only a single file output. 54 | -s | --single ) mode=single; shift ;; 55 | # If appropriate use only a single file output. 56 | -c | --chaptered ) mode=chaptered; shift ;; 57 | # This is the same as --single option. 58 | -e:mp3 ) codec=libmp3lame; extension=mp3; mode=single; container=mp3; shift ;; 59 | # Identical to --acc option. 60 | -e:m4a | -a | --aac ) codec=copy; extension=m4a; mode=single; container=mp4; shift ;; 61 | # Similar to --aac but specific to audio books 62 | -e:m4b ) codec=copy; extension=m4b; mode=single; container=mp4; shift ;; 63 | # Change the working dir from AAX directory to what you choose. 64 | -t | --target_dir ) targetdir="$2"; shift 2 ;; 65 | # Use a custom directory naming scheme, with variables. 66 | -D | --dir-naming-scheme ) dirNameScheme="$2"; customDNS=1; shift 2 ;; 67 | # Use a custom file naming scheme, with variables. 68 | -F | --file-naming-scheme ) fileNameScheme="$2"; customFNS=1; shift 2 ;; 69 | # Use a custom chapter naming scheme, with variables. 70 | --chapter-naming-scheme ) chapterNameScheme="$2"; customCNS=1; shift 2 ;; 71 | # Move the AAX file to a new directory when decoding is complete. 72 | -C | --complete_dir ) completedir="$2"; shift 2 ;; 73 | # Authorization code associate with the AAX file(s) 74 | -A | --authcode ) auth_code="$2"; shift 2 ;; 75 | # Don't overwrite the target directory if it already exists 76 | -n | --no-clobber ) noclobber=1; shift ;; 77 | # Extremely verbose output. 78 | -d | --debug ) loglevel=3; shift ;; 79 | # Set loglevel. 80 | -l | --loglevel ) loglevel="$2"; shift 2 ;; 81 | # Validate ONLY the aax file(s) No transcoding occurs 82 | -V | --validate ) VALIDATE=1; shift ;; 83 | # continue splitting chapters at chapter continueAt 84 | --continue ) continueAt="$2"; continue=1; shift 2 ;; 85 | # Use additional data got with mkb79/audible-cli 86 | --use-audible-cli-data ) audibleCli=1; shift ;; 87 | # Path of the library-file, generated by mkb79/audible-cli (audible library export -o ./library.tsv) 88 | -L | --audible-cli-library-file ) library_file="$2"; shift 2 ;; 89 | # Compression level 90 | --level ) level="$2"; shift 2 ;; 91 | # Keep author number n 92 | --keep-author ) keepArtist="$2"; shift 2 ;; 93 | # Author override 94 | --author ) authorOverride="$2"; shift 2 ;; 95 | # Ffmpeg path override 96 | --ffmpeg-path ) ffmpegPath="$2"; shift 2 ;; 97 | # Ffmpeg name override 98 | --ffmpeg-name ) ffmpegName="$2"; shift 2 ;; 99 | # Ffprobe name override 100 | --ffprobe-name ) ffprobeName="$2"; shift 2 ;; 101 | # Command synopsis. 102 | -h | --help ) printf "$usage" $0 ; exit ;; 103 | # Standard flag signifying the end of command line processing. 104 | -- ) shift; break ;; 105 | # Anything else stops command line processing. 106 | * ) break ;; 107 | 108 | esac 109 | done 110 | 111 | # ----- 112 | # Empty argv means we have nothing to do so lets bark some help. 113 | if [ "$#" -eq 0 ]; then 114 | printf "$usage" $0 115 | exit 1 116 | fi 117 | 118 | # Setup safer bash script defaults. 119 | set -o errexit -o noclobber -o nounset -o pipefail 120 | 121 | # ======================================================================== 122 | # Utility Functions 123 | 124 | # ----- 125 | # debug 126 | # debug "Some longish message" 127 | debug() { 128 | if [ $loglevel == 3 ] ; then 129 | echo "$(date "+%F %T%z") DEBUG ${1}" 130 | fi 131 | } 132 | 133 | # ----- 134 | # debug dump contents of a file to STDOUT 135 | # debug "" 136 | debug_file() { 137 | if [ $loglevel == 3 ] ; then 138 | echo "$(date "+%F %T%z") DEBUG" 139 | echo "=Start==========================================================================" 140 | cat "${1}" 141 | echo "=End============================================================================" 142 | fi 143 | } 144 | 145 | # ----- 146 | # debug dump a list of internal script variables to STDOUT 147 | # debug_vars "Some Message" var1 var2 var3 var4 var5 148 | debug_vars() { 149 | if [ $loglevel == 3 ] ; then 150 | msg="$1"; shift ; # Grab the message 151 | args=("$@") # Grab the rest of the args 152 | 153 | # determine the length of the longest key 154 | l=0 155 | for (( n=0; n<${#args[@]}; n++ )) ; do 156 | (( "${#args[$n]}" > "$l" )) && l=${#args[$n]} 157 | done 158 | 159 | # Print the Debug Message 160 | echo "$(date "+%F %T%z") DEBUG ${msg}" 161 | echo "=Start==========================================================================" 162 | 163 | # Using the max length of a var name we dynamically create the format. 164 | fmt="%-"${l}"s = %s\n" 165 | 166 | for (( n=0; n<${#args[@]}; n++ )) ; do 167 | eval val="\$${args[$n]}" ; # We save off the value of the var in question for ease of coding. 168 | 169 | echo "$(printf "${fmt}" ${args[$n]} "${val}" )" 170 | done 171 | echo "=End============================================================================" 172 | fi 173 | } 174 | 175 | # ----- 176 | # log 177 | log() { 178 | if [ "$((${loglevel} > 1))" == "1" ] ; then 179 | echo "$(date "+%F %T%z") ${1}" 180 | else 181 | echo "${1}" 182 | fi 183 | } 184 | 185 | # ----- 186 | #progressbar produces a progressbar in the style of 187 | # process: |####### | XX% (part/total unit) 188 | # which is gonna be overwritten by the next line. 189 | 190 | progressbar() { 191 | #get input 192 | part=${1} 193 | total=${2} 194 | 195 | #compute percentage and make print_percentage the same length regardless of the number of digits. 196 | percentage=$((part*100/total)) 197 | if [ "$((percentage<10))" = "1" ]; then print_percentage=" $percentage" 198 | elif [ "$((percentage<100))" = "1" ]; then print_percentage=" $percentage" 199 | else print_percentage="$percentage"; fi 200 | 201 | #draw progressbar with one # for every 5% and blank spaces for the missing part. 202 | progressbar="" 203 | for (( n=0; n<(percentage/5); n++ )) ; do progressbar="$progressbar#"; done 204 | for (( n=0; n<(20-(percentage/5)); n++ )) ; do progressbar="$progressbar "; done 205 | 206 | #print progressbar 207 | echo -ne "Chapter splitting: |$progressbar| $print_percentage% ($part/$total chapters)\r" 208 | } 209 | # Print out what we have already after command line processing. 210 | debug_vars "Command line options as set" codec extension mode container targetdir completedir auth_code keepArtist authorOverride audibleCli 211 | 212 | # ======================================================================== 213 | # Variable validation 214 | 215 | if [ $(uname) = 'Linux' ]; then 216 | GREP="grep" 217 | FIND="find" 218 | SED="sed" 219 | else 220 | GREP="ggrep" 221 | FIND="gfind" 222 | SED="gsed" 223 | fi 224 | 225 | # Use custom ffmpeg (and ffprobe) binary ( --ffmpeg-path flag) 226 | if [ -n "$ffmpegPath" ]; then 227 | FFMPEG="$ffmpegPath/${ffmpegName}" 228 | FFPROBE="$ffmpegPath/${ffprobeName}" 229 | else 230 | FFMPEG="${ffmpegName}" 231 | FFPROBE="${ffprobeName}" 232 | fi 233 | 234 | debug_vars "ffmpeg/ffprobe paths" FFMPEG FFPROBE 235 | 236 | # ----- 237 | # Detect which annoying version of grep we have 238 | if ! [[ $(type -P "$GREP") ]]; then 239 | echo "$GREP (GNU grep) is not in your PATH" 240 | echo "Without it, this script will break." 241 | echo "On macOS, you may want to try: brew install grep" 242 | exit 1 243 | fi 244 | 245 | # ----- 246 | # Detect which annoying version of find we have 247 | if ! [[ $(type -P "$FIND") ]]; then 248 | echo "$FIND (GNU find) is not in your PATH" 249 | echo "Without it, this script will break." 250 | echo "On macOS, you may want to try: brew install findutils" 251 | exit 1 252 | fi 253 | 254 | # ----- 255 | # Detect which annoying version of sed we have 256 | if ! [[ $(type -P "$SED") ]]; then 257 | echo "$SED (GNU sed) is not in your PATH" 258 | echo "Without it, this script will break." 259 | echo "On macOS, you may want to try: brew install gnu-sed" 260 | exit 1 261 | fi 262 | 263 | # ----- 264 | # Detect ffmpeg and ffprobe 265 | if [[ "x$(type -P "$FFMPEG")" == "x" ]]; then 266 | echo "ERROR ffmpeg was not found on your env PATH variable" 267 | echo "Without it, this script will break." 268 | echo "INSTALL:" 269 | echo "MacOS: brew install ffmpeg" 270 | echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc" 271 | echo "Ubuntu (20.04): sudo apt-get update; sudo apt-get install ffmpeg x264 x265 bc" 272 | echo "RHEL: yum install ffmpeg" 273 | exit 1 274 | fi 275 | 276 | # ----- 277 | # Detect ffmpeg and ffprobe 278 | if [[ "x$(type -P "$FFPROBE")" == "x" ]]; then 279 | echo "ERROR ffprobe was not found on your env PATH variable" 280 | echo "Without it, this script will break." 281 | echo "INSTALL:" 282 | echo "MacOS: brew install ffmpeg" 283 | echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc" 284 | echo "RHEL: yum install ffmpeg" 285 | exit 1 286 | fi 287 | 288 | 289 | # ----- 290 | # Detect if we need mp4art for cover additions to m4a & m4b files. 291 | if [[ "x${container}" == "xmp4" && "x$(type -P mp4art)" == "x" ]]; then 292 | echo "WARN mp4art was not found on your env PATH variable" 293 | echo "Without it, this script will not be able to add cover art to" 294 | echo "m4b files. Note if there are no other errors the AAXtoMP3 will" 295 | echo "continue. However no cover art will be added to the output." 296 | echo "INSTALL:" 297 | echo "MacOS: brew install mp4v2" 298 | echo "Ubuntu: sudo apt-get install mp4v2-utils" 299 | fi 300 | 301 | # ----- 302 | # Detect if we need mp4chaps for adding chapters to m4a & m4b files. 303 | if [[ "x${container}" == "xmp4" && "x$(type -P mp4chaps)" == "x" ]]; then 304 | echo "WARN mp4chaps was not found on your env PATH variable" 305 | echo "Without it, this script will not be able to add chapters to" 306 | echo "m4a/b files. Note if there are no other errors the AAXtoMP3 will" 307 | echo "continue. However no chapter data will be added to the output." 308 | echo "INSTALL:" 309 | echo "MacOS: brew install mp4v2" 310 | echo "Ubuntu: sudo apt-get install mp4v2-utils" 311 | fi 312 | 313 | # ----- 314 | # Detect if we need mediainfo for adding description and narrator 315 | if [[ "x$(type -P mediainfo)" == "x" ]]; then 316 | echo "WARN mediainfo was not found on your env PATH variable" 317 | echo "Without it, this script will not be able to add the narrator" 318 | echo "and description tags. Note if there are no other errors the AAXtoMP3" 319 | echo "will continue. However no such tags will be added to the output." 320 | echo "INSTALL:" 321 | echo "MacOS: brew install mediainfo" 322 | echo "Ubuntu: sudo apt-get install mediainfo" 323 | fi 324 | 325 | # ----- 326 | # Obtain the authcode from either the command line, local directory or home directory. 327 | # See Readme.md for details on how to acquire your personal authcode for your personal 328 | # audible AAX files. 329 | if [ -z $auth_code ]; then 330 | if [ -r .authcode ]; then 331 | auth_code=`head -1 .authcode` 332 | elif [ -r ~/.authcode ]; then 333 | auth_code=`head -1 ~/.authcode` 334 | fi 335 | fi 336 | 337 | # ----- 338 | # Check the target dir for if set if it is writable 339 | if [[ "x${targetdir}" != "x" ]]; then 340 | if [[ ! -w "${targetdir}" || ! -d "${targetdir}" ]] ; then 341 | echo "ERROR Target Directory does not exist or is not writable: \"$targetdir\"" 342 | echo "$usage" 343 | exit 1 344 | fi 345 | fi 346 | 347 | # ----- 348 | # Check the completed dir for if set if it is writable 349 | if [[ "x${completedir}" != "x" ]]; then 350 | if [[ ! -w "${completedir}" || ! -d "${completedir}" ]] ; then 351 | echo "ERROR Complete Directory does not exist or is not writable: \"$completedir\"" 352 | echo "$usage" 353 | exit 1 354 | fi 355 | fi 356 | 357 | # ----- 358 | # Check whether the loglevel is valid 359 | if [ "$((${loglevel} < 0 || ${loglevel} > 3 ))" = "1" ]; then 360 | echo "ERROR loglevel has to be in the range from 0 to 3!" 361 | echo " 0: Show progress only" 362 | echo " 1: default" 363 | echo " 2: a little more information, timestamps" 364 | echo " 3: debug" 365 | echo "$usage" 366 | exit 1 367 | fi 368 | # ----- 369 | # If a compression level is given, check whether the given codec supports compression level specifiers and whether the level is valid. 370 | if [ "${level}" != "-1" ]; then 371 | if [ "${codec}" == "flac" ]; then 372 | if [ "$((${level} < 0 || ${level} > 12 ))" = "1" ]; then 373 | echo "ERROR Flac compression level has to be in the range from 0 to 12!" 374 | echo "$usage" 375 | exit 1 376 | fi 377 | elif [ "${codec}" == "libopus" ]; then 378 | if [ "$((${level} < 0 || ${level} > 10 ))" = "1" ]; then 379 | echo "ERROR Opus compression level has to be in the range from 0 to 10!" 380 | echo "$usage" 381 | exit 1 382 | fi 383 | elif [ "${codec}" == "libmp3lame" ]; then 384 | if [ "$((${level} < 0 || ${level} > 9 ))" = "1" ]; then 385 | echo "ERROR MP3 compression level has to be in the range from 0 to 9!" 386 | echo "$usage" 387 | exit 1 388 | fi 389 | else 390 | echo "ERROR This codec doesnt support compression levels!" 391 | echo "$usage" 392 | exit 1 393 | fi 394 | fi 395 | 396 | # ----- 397 | # Clean up if someone hits ^c or the script exits for any reason. 398 | trap 'rm -r -f "${working_directory}"' EXIT 399 | 400 | # ----- 401 | # Set up some basic working files ASAP. Note the trap will clean this up no matter what. 402 | working_directory=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` 403 | metadata_file="${working_directory}/metadata.txt" 404 | 405 | # ----- 406 | # Validate the AAX and extract the metadata associated with the file. 407 | validate_aax() { 408 | local media_file 409 | media_file="$1" 410 | 411 | # Test for existence 412 | if [[ ! -r "${media_file}" ]] ; then 413 | log "ERROR File NOT Found: ${media_file}" 414 | return 1 415 | else 416 | if [[ "${VALIDATE}" == "1" ]]; then 417 | log "Test 1 SUCCESS: ${media_file}" 418 | fi 419 | fi 420 | 421 | # Clear the errexit value we want to capture the output of the ffprobe below. 422 | set +e errexit 423 | 424 | # Take a look at the aax file and see if it is valid. If the source file is aaxc, we give ffprobe additional flags 425 | output="$("$FFPROBE" -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)" 426 | 427 | # If invalid then say something. 428 | if [[ $? != "0" ]] ; then 429 | # No matter what lets bark that something is wrong. 430 | log "ERROR: Invalid File: ${media_file}" 431 | elif [[ "${VALIDATE}" == "1" ]]; then 432 | # If the validate option is present then lets at least state what is valid. 433 | log "Test 2 SUCCESS: ${media_file}" 434 | fi 435 | 436 | # This is a big test only performed when the --validate switch is passed. 437 | if [[ "${VALIDATE}" == "1" ]]; then 438 | output="$("$FFMPEG" -hide_banner ${decrypt_param} -i "${media_file}" -vn -f null - 2>&1)" 439 | if [[ $? != "0" ]] ; then 440 | log "ERROR: Invalid File: ${media_file}" 441 | else 442 | log "Test 3 SUCCESS: ${media_file}" 443 | fi 444 | fi 445 | 446 | # Dump the output of the ffprobe command. 447 | debug "$output" 448 | 449 | # Turn it back on. ffprobe is done. 450 | set -e errexit 451 | } 452 | 453 | validate_extra_files() { 454 | local extra_media_file extra_find_command 455 | extra_media_file="$1" 456 | # Bash trick to delete, non greedy, from the end up until the first '-' 457 | extra_title="${extra_media_file%-*}" 458 | 459 | # Using this is not ideal, because if the naming scheme is changed then 460 | # this part of the script will break 461 | # AAX file: BookTitle-LC_128_44100_stereo.aax 462 | # Cover file: BookTitle_(1215).jpg 463 | # Chapter file: BookTitle-chapters.json 464 | 465 | # Chapter 466 | extra_chapter_file="${extra_title}-chapters.json" 467 | 468 | # Cover 469 | extra_dirname="$(dirname "${extra_media_file}")" 470 | extra_find_command='$FIND "${extra_dirname}" -maxdepth 1 -regex ".*/${extra_title##*/}_([0-9]+)\.jpg"' 471 | # We want the output of the find command, we will turn errexit on later 472 | set +e errexit 473 | extra_cover_file="$(eval ${extra_find_command})" 474 | extra_eval_comm="$(eval echo ${extra_find_command})" 475 | set -e errexit 476 | 477 | if [[ "${aaxc}" == "1" ]]; then 478 | # bash trick to get file w\o extention (delete from end to the first '.') 479 | extra_voucher="${extra_media_file%.*}.voucher" 480 | if [[ ! -r "${extra_voucher}" ]] ; then 481 | log "ERROR File NOT Found: ${extra_voucher}" 482 | return 1 483 | fi 484 | aaxc_key=$(jq -r '.content_license.license_response.key' "${extra_voucher}") 485 | aaxc_iv=$(jq -r '.content_license.license_response.iv' "${extra_voucher}") 486 | fi 487 | 488 | 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 489 | 490 | # Test for chapter file existence 491 | if [[ ! -r "${extra_chapter_file}" ]] ; then 492 | log "ERROR File NOT Found: ${extra_chapter_file}" 493 | return 1 494 | fi 495 | if [[ "x${extra_cover_file}" == "x" ]] ; then 496 | log "ERROR Cover File NOT Found" 497 | return 1 498 | fi 499 | 500 | # Test for library file 501 | if [[ ! -r "${library_file}" ]] ; then 502 | library_file_exists=0 503 | debug "library file not found" 504 | else 505 | library_file_exists=1 506 | debug "library file found" 507 | fi 508 | 509 | debug "All expected audible-cli related file are here" 510 | } 511 | 512 | # ----- 513 | # Inspect the AAX and extract the metadata associated with the file. 514 | save_metadata() { 515 | local media_file 516 | media_file="$1" 517 | "$FFPROBE" -i "$media_file" 2> "$metadata_file" 518 | if [[ $(type -P mediainfo) ]]; then 519 | echo "Mediainfo data START" >> "$metadata_file" 520 | # Mediainfo output is structured like ffprobe, so we append it to the metadata file and then parse it with get_metadata_value() 521 | # mediainfo "$media_file" >> "$metadata_file" 522 | # Or we only get the data we are intrested in: 523 | # Description 524 | echo "Track_More :" "$(mediainfo --Inform="General;%Track_More%" "$media_file")" >> "$metadata_file" 525 | # Narrator 526 | echo "nrt :" "$(mediainfo --Inform="General;%nrt%" "$media_file")" >> "$metadata_file" 527 | # Publisher 528 | echo "pub :" "$(mediainfo --Inform="General;%pub%" "$media_file")" >> "$metadata_file" 529 | echo "Mediainfo data END" >> "$metadata_file" 530 | fi 531 | if [[ "${audibleCli}" == "1" ]]; then 532 | # If we use data we got with audible-cli, we delete conflicting chapter infos 533 | $SED -i '/^ Chapter #/d' "${metadata_file}" 534 | # Some magic: we parse the .json generated by audible-cli. 535 | # to get the output structure like the one generated by ffprobe, 536 | # we use some characters (#) as placeholder, add some new lines, 537 | # put a ',' after the start value, we calculate the end of each chapter 538 | # as start+length, and we convert (divide) the time stamps from ms to s. 539 | # Then we delete all ':' and '/' since they make a filename invalid. 540 | 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}" \ 541 | | $SED 's@[:/]@@g' >> "$metadata_file" 542 | # In case we want to use a single file m4b we need to extract the 543 | # chapter titles from the .json generated by audible–cli and store 544 | # them correctly formatted for mp4chaps in a chapter.txt 545 | if [ "${mode}" == "single" ]; then 546 | # Creating a temp file to store the chapter data collected in save_metadata, as the output 547 | # folder will only be defined after save_metadata has been executed. 548 | # This file is only required when using audible-cli data and executing in single mode to 549 | # get proper chapter titles in single file m4b output. 550 | tmp_chapter_file="${working_directory}/chapter.txt" 551 | jq -r \ 552 | 'def pad(n): tostring | if (n > length) then ((n - length) * "0") + . else . end; 553 | .content_metadata.chapter_info.chapters | 554 | reduce .[] as $c ([]; if $c.chapters? then .+[$c | del(.chapters)]+[$c.chapters] else .+[$c] end) | flatten | 555 | to_entries | 556 | .[] | 557 | "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))) 558 | CHAPTER\((.key))NAME=\(.value.title)"' "${extra_chapter_file}" > "${tmp_chapter_file}" 559 | fi 560 | 561 | # get extra meta data from library.tsv 562 | if [[ "${library_file_exists}" == 1 ]]; then 563 | asin=$(jq -r '.content_metadata.content_reference.asin' "${extra_chapter_file}") 564 | if [[ ! -z "${asin}" ]]; then 565 | lib_entry=$($GREP "^${asin}" "${library_file}") 566 | if [[ ! -z "${lib_entry}" ]]; then 567 | series_title=$(echo "${lib_entry}" | awk -F '\t' '{print $6}') 568 | series_sequence=$(echo "${lib_entry}" | awk -F '\t' '{print $7}') 569 | $SED -i "/^ Metadata:/a\\ 570 | series : ${series_title}\\ 571 | series_sequence : ${series_sequence}" "${metadata_file}" 572 | fi 573 | fi 574 | fi 575 | fi 576 | debug "Metadata file $metadata_file" 577 | debug_file "$metadata_file" 578 | } 579 | 580 | # ----- 581 | # Reach into the meta data and extract a specific value. 582 | # This is a long pipe of transforms. 583 | # This finds the first occurrence of the key : value pair. 584 | get_metadata_value() { 585 | local key 586 | key="$1" 587 | # Find the key in the meta data file # Extract field value # Remove the following /'s "(Unabridged) at start end and multiples. 588 | 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')" 589 | } 590 | 591 | # ----- 592 | # specific variant of get_metadata_value bitrate is important for transcoding. 593 | get_bitrate() { 594 | get_metadata_value bitrate | $GREP --only-matching '[0-9]\+' 595 | } 596 | 597 | # Save the original value, since in the for loop we overwrite 598 | # $audibleCli in case the file is aaxc. If the file is the 599 | # old aax, reset the variable to be the one passed by the user 600 | originalAudibleCliVar=$audibleCli 601 | # ======================================================================== 602 | # Main Transcode Loop 603 | for aax_file 604 | do 605 | # If the file is in aaxc format, set the proper variables 606 | if [[ ${aax_file##*.} == "aaxc" ]]; then 607 | # File is the new .aaxc 608 | aaxc=1 609 | audibleCli=1 610 | else 611 | # File is the old .aax 612 | aaxc=0 613 | # If some previous file in the loop are aaxc, the $audibleCli variable has been overwritten, so we reset it to the original one 614 | audibleCli=$originalAudibleCliVar 615 | fi 616 | 617 | debug_vars "Variables set based on file extention" aaxc originalAudibleCliVar audibleCli 618 | 619 | # No point going on if no authcode found and the file is aax. 620 | # If we use aaxc as input, we do not need it 621 | # if the string $auth_code is null and the format is not aaxc; quit. We need the authcode 622 | if [ -z $auth_code ] && [ "${aaxc}" = "0" ]; then 623 | echo "ERROR Missing authcode, can't decode $aax_file" 624 | echo "$usage" 625 | exit 1 626 | fi 627 | 628 | # Validate the input aax file. Note this happens no matter what. 629 | # It's just that if the validate option is set then we skip to next file. 630 | # If however validate is not set and we proceed with the script any errors will 631 | # case the script to stop. 632 | 633 | # If the input file is aaxc, we need to first get the audible_key and audible_iv 634 | # We get them in the function validate_extra_files 635 | 636 | if [[ ${audibleCli} == "1" ]] ; then 637 | # If we have additional files (obtained via audible-cli), be sure that they 638 | # exists and they are in the correct location. 639 | validate_extra_files "${aax_file}" 640 | fi 641 | 642 | # Set the needed params to decrypt the file. Needed in all command that require ffprobe or ffmpeg 643 | # After validate_extra_files, since the -audible_key and -audible_iv are read in that function 644 | if [[ ${aaxc} == "1" ]] ; then 645 | decrypt_param="-audible_key ${aaxc_key} -audible_iv ${aaxc_iv}" 646 | else 647 | decrypt_param="-activation_bytes ${auth_code}" 648 | fi 649 | 650 | validate_aax "${aax_file}" 651 | if [[ ${VALIDATE} == "1" ]] ; then 652 | # Don't bother doing anything else with this file. 653 | continue 654 | fi 655 | 656 | # ----- 657 | # Make sure everything is a variable. Simplifying Command interpretation 658 | save_metadata "${aax_file}" 659 | genre=$(get_metadata_value genre) 660 | if [ "x${authorOverride}" != "x" ]; then 661 | #Manual Override 662 | artist="${authorOverride}" 663 | album_artist="${authorOverride}" 664 | else 665 | if [ "${keepArtist}" != "-1" ]; then 666 | # Choose artist from the one that are present in the metadata. Comma separated list of names 667 | # remove leading space; 'C. S. Lewis' -> 'C.S. Lewis' 668 | artist="$(get_metadata_value artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')" 669 | album_artist="$(get_metadata_value album_artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')" 670 | else 671 | # The default 672 | artist=$(get_metadata_value artist) 673 | album_artist="$(get_metadata_value album_artist)" 674 | fi 675 | fi 676 | title=$(get_metadata_value title) 677 | title=${title:0:128} 678 | bitrate="$(get_bitrate)k" 679 | album="$(get_metadata_value album)" 680 | album_date="$(get_metadata_value date)" 681 | copyright="$(get_metadata_value copyright)" 682 | series="$(get_metadata_value series)" 683 | series_sequence="$(get_metadata_value series_sequence)" 684 | 685 | # Get more tags with mediainfo 686 | if [[ $(type -P mediainfo) ]]; then 687 | narrator="$(get_metadata_value nrt)" 688 | description="$(get_metadata_value Track_More)" 689 | publisher="$(get_metadata_value pub)" 690 | else 691 | narrator="" 692 | description="" 693 | publisher="" 694 | fi 695 | 696 | # Define the output_directory 697 | if [ "${customDNS}" == "1" ]; then 698 | currentDirNameScheme="$(eval echo "${dirNameScheme}")" 699 | else 700 | # The Default 701 | currentDirNameScheme="${genre}/${artist}/${title}" 702 | fi 703 | 704 | # If we defined a target directory, use it. Otherwise use the location of the AAX file 705 | if [ "x${targetdir}" != "x" ] ; then 706 | output_directory="${targetdir}/${currentDirNameScheme}" 707 | else 708 | output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}" 709 | fi 710 | 711 | # Define the output_file 712 | if [ "${customFNS}" == "1" ]; then 713 | currentFileNameScheme="$(eval echo "${fileNameScheme}")" 714 | else 715 | # The Default 716 | currentFileNameScheme="${title}" 717 | fi 718 | output_file="${output_directory}/${currentFileNameScheme}.${extension}" 719 | 720 | if [[ "${noclobber}" = "1" ]] && [[ -d "${output_directory}" ]]; then 721 | log "Noclobber enabled but directory '${output_directory}' exists. Skipping to avoid overwriting" 722 | rm -f "${metadata_file}" "${tmp_chapter_file}" 723 | continue 724 | fi 725 | mkdir -p "${output_directory}" 726 | 727 | if [ "$((${loglevel} > 0))" = "1" ]; then 728 | # Fancy declaration of which book we are decoding. Including the AUTHCODE. 729 | dashline="----------------------------------------------------" 730 | log "$(printf '\n----Decoding---%s%s--%s--' "${title}" "${dashline:${#title}}" "${auth_code}")" 731 | log "Source: ${aax_file}" 732 | fi 733 | 734 | # Big long DEBUG output. Fully describes the settings used for transcoding. 735 | # Note this is a long debug command. It's not critical to operation. It's purely for people debugging 736 | # and coders wanting to extend the script. 737 | 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 738 | 739 | 740 | # Display the total length of the audiobook in format hh:mm:ss 741 | # 10#$var force base-10 interpretation. By default it's base-8, so values like 08 or 09 are not octal numbers 742 | total_length="$("$FFPROBE" -v error ${decrypt_param} -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${aax_file}" | cut -d . -f 1)" 743 | hours="$((total_length/3600))" 744 | if [ "$((hours<10))" = "1" ]; then hours="0$hours"; fi 745 | minutes="$((total_length/60-60*10#$hours))" 746 | if [ "$((minutes<10))" = "1" ]; then minutes="0$minutes"; fi 747 | seconds="$((total_length-3600*10#$hours-60*10#$minutes))" 748 | if [ "$((seconds<10))" = "1" ]; then seconds="0$seconds"; fi 749 | log "Total length: $hours:$minutes:$seconds" 750 | 751 | # If level != -1 specify a compression level in ffmpeg. 752 | compression_level_param="" 753 | if [ "${level}" != "-1" ]; then 754 | compression_level_param="-compression_level ${level}" 755 | fi 756 | 757 | # ----- 758 | if [ "${continue}" == "0" ]; then 759 | # This is the main work horse command. This is the primary transcoder. 760 | # This is the primary transcode. All the heavy lifting is here. 761 | 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}"' 762 | 0))" == "1" ]; then 787 | log "Created ${output_file}." 788 | fi 789 | # ----- 790 | fi 791 | # Grab the cover art if available. 792 | cover_file="${output_directory}/${currentFileNameScheme}.jpg" 793 | if [ "${continue}" == "0" ]; then 794 | if [ "${audibleCli}" == "1" ]; then 795 | # We have a better quality cover file, copy it. 796 | if [ "$((${loglevel} > 1))" == "1" ]; then 797 | log "Copy cover file to ${cover_file}..." 798 | fi 799 | cp "${extra_cover_file}" "${cover_file}" 800 | else 801 | # Audible-cli not used, extract the cover from the aax file 802 | if [ "$((${loglevel} > 1))" == "1" ]; then 803 | log "Extracting cover into ${cover_file}..." 804 | fi 805 | &1 | $GREP -Po "[0-9]+(?=x[0-9]+)" | tail -n 1) 811 | if (( ${cover_width} % 2 == 1 )); then 812 | if [ "$((${loglevel} > 1))" == "1" ]; then 813 | log "Cover ${cover_file} has odd width ${cover_width}, setting extra_crop_cover to make even." 814 | fi 815 | # We now set a variable, ${extra_crop_cover}, which contains an additional 816 | # ffmpeg flag. It crops the cover so the width and the height is divisible by two. 817 | # Set the flag only if we use a cover art with an odd width. 818 | extra_crop_cover='-vf crop=trunc(iw/2)*2:trunc(ih/2)*2' 819 | fi 820 | 821 | # ----- 822 | # If mode=chaptered, split the big converted file by chapter and remove it afterwards. 823 | # Not all audio encodings make sense with multiple chapter outputs (see options section) 824 | if [ "${mode}" == "chaptered" ]; then 825 | # Playlist m3u support 826 | playlist_file="${output_directory}/${currentFileNameScheme}.m3u" 827 | if [ "${continue}" == "0" ]; then 828 | if [ "$((${loglevel} > 0))" == "1" ]; then 829 | log "Creating PlayList ${currentFileNameScheme}.m3u" 830 | fi 831 | echo '#EXTM3U' > "${playlist_file}" 832 | fi 833 | 834 | # Determine the number of chapters. 835 | chaptercount=$($GREP -Pc "Chapter.*start.*end" $metadata_file) 836 | if [ "$((${loglevel} > 0))" == "1" ]; then 837 | log "Extracting ${chaptercount} chapter files from ${output_file}..." 838 | if [ "${continue}" == "1" ]; then 839 | log "Continuing at chapter ${continueAt}:" 840 | fi 841 | fi 842 | chapternum=1 843 | #start progressbar for loglevel 0 and 1 844 | if [ "$((${loglevel} < 2))" == "1" ]; then 845 | progressbar 0 ${chaptercount} 846 | fi 847 | # We pipe the metadata_file in read. 848 | # Example of the section that we are interested in: 849 | # 850 | # Chapter #0:0: start 0.000000, end 1928.231474 851 | # Metadata: 852 | # title : Chapter 1 853 | # 854 | # Then read the line in these variables: 855 | # first Chapter 856 | # _ #0:0: 857 | # _ start 858 | # chapter_start 0.000000, 859 | # _ end 860 | # chapter_end 1928.231474 861 | while read -r -u9 first _ _ chapter_start _ chapter_end 862 | do 863 | # Do things only if the line starts with 'Chapter' 864 | if [[ "${first}" = "Chapter" ]]; then 865 | # The next line (Metadata:...) gets discarded 866 | read -r -u9 _ 867 | # From the line 'title : Chapter 1' we save the third field and those after in chapter 868 | read -r -u9 _ _ chapter 869 | 870 | # The formatting of the chapters names and the file names. 871 | # Chapter names are used in a few place. 872 | # Define the chapter_file 873 | if [ "${customCNS}" == "1" ]; then 874 | chapter_title="$(eval echo "${chapterNameScheme}")" 875 | else 876 | # The Default 877 | chapter_title="${title}-$(printf %0${#chaptercount}d $chapternum) ${chapter}" 878 | fi 879 | chapter_file="${output_directory}/${chapter_title}.${extension}" 880 | 881 | # Since the .aax file allready got converted we can use 882 | # -acodec copy, which is much faster than a reencodation. 883 | # Since there is an issue when using copy on flac, where 884 | # the duration of the chapters gets shown as if they where 885 | # as long as the whole audiobook. 886 | chapter_codec="" 887 | if test "${extension}" = "flac"; then 888 | chapter_codec="flac "${compression_level_param}"" 889 | else 890 | chapter_codec="copy" 891 | fi 892 | 893 | #Since there seems to be a bug in some older versions of ffmpeg, which makes, that -ss and -to 894 | #have to be apllied to the output file, this makes, that -ss and -to get applied on the input for 895 | #ffmpeg version 4+ and on the output for all older versions. 896 | split_input="" 897 | split_output="" 898 | if [ "$(($("$FFMPEG" -version | $SED -E 's/[^0-9]*([0-9]).*/\1/g;1q') > 3))" = "1" ]; then 899 | split_input="-ss ${chapter_start%?} -to ${chapter_end}" 900 | else 901 | split_output="-ss ${chapter_start%?} -to ${chapter_end}" 902 | fi 903 | 904 | # Big Long chapter debug 905 | debug_vars "Chapter Variables:" cover_file chapter_start chapter_end chapternum chapter chapterNameScheme chapter_title chapter_file 906 | if [ "$((${continueAt} > ${chapternum}))" = "0" ]; then 907 | # Extract chapter by time stamps start and finish of chapter. 908 | # This extracts based on time stamps start and end. 909 | if [ "$((${loglevel} > 1))" == "1" ]; then 910 | log "Splitting chapter ${chapternum}/${chaptercount} start:${chapter_start%?}(s) end:${chapter_end}(s)" 911 | fi 912 | > "${playlist_file}" 939 | echo "${chapter_title}.${extension}" >> "${playlist_file}" 940 | fi 941 | chapternum=$((chapternum + 1 )) 942 | fi 943 | done 9< "$metadata_file" 944 | 945 | # Clean up of working directory stuff. 946 | rm "${output_file}" 947 | if [ "$((${loglevel} > 1))" == "1" ]; then 948 | log "Done creating chapters for ${output_directory}." 949 | else 950 | #ending progress bar 951 | echo "" 952 | fi 953 | else 954 | # Perform file tasks on output file. 955 | # ---- 956 | # ffmpeg seems to copy only chapter position, not chapter names. 957 | # use already created chapter.txt from save_metadata() in case 958 | # audible-cli data is used else use ffprobe to extract from m4b 959 | if [[ ${container} == "mp4" && $(type -P mp4chaps) ]]; then 960 | if [ "${audibleCli}" == "1" ]; then 961 | mv "${tmp_chapter_file}" "${output_directory}/${currentFileNameScheme}.chapters.txt" 962 | else 963 | "$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" 964 | fi 965 | $SED -i 's/\,000/\.000/' "${output_directory}/${currentFileNameScheme}.chapters.txt" 966 | mp4chaps -i "${output_file}" 967 | fi 968 | fi 969 | 970 | if [ -f "${cover_file}" ]; then 971 | log "Adding cover art" 972 | # FFMPEG does not support MPEG-4 containers fully # 973 | if [ "${container}" == "mp4" ] ; then 974 | mp4art --add "${cover_file}" "${output_file}" 975 | # FFMPEG for everything else # 976 | else 977 | # Create temporary output file name - ensure extention matches previous appropriate output file to keep ffmpeg happy 978 | cover_output_file="${output_file%.*}.cover.${output_file##*.}" 979 | # Copy audio stream from current output, and video stream from cover file, setting appropriate metadata 980 | 0))" == "1" ]; then 1000 | log "Complete ${title}" 1001 | fi 1002 | # Lastly get rid of any extra stuff. 1003 | rm "${metadata_file}" 1004 | 1005 | # Move the aax file if the decode is completed and the --complete_dir is set to a valid location. 1006 | # Check the target dir for if set if it is writable 1007 | if [[ "x${completedir}" != "x" ]]; then 1008 | if [ "$((${loglevel} > 0))" == "1" ]; then 1009 | log "Moving Transcoded ${aax_file} to ${completedir}" 1010 | fi 1011 | mv "${aax_file}" "${completedir}" 1012 | fi 1013 | 1014 | done 1015 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (c) 2015 KrumpetPirate 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAXtoMP3 2 | The purpose of this software is to convert AAX (or AAXC) files to common MP3, M4A, M4B, flac and ogg formats 3 | through a basic bash script frontend to FFMPEG. 4 | 5 | Audible uses the AAX file format to maintain DRM restrictions on their audio 6 | books and if you download your book through your library it will be 7 | stored in this format. 8 | 9 | The purpose of this software is **not** to circumvent the DRM restrictions 10 | for audio books that **you** do not own in that you do not have them on 11 | your **personal** Audible account. The purpose of this software is to 12 | create a method for you to download and store your books just in case 13 | Audible fails for some reason. 14 | 15 | ## Requirements 16 | * bash 3.2.57 or later tested 17 | * ffmpeg version 2.8.3 or later (4.4 or later if the input file is `.aaxc`) 18 | * libmp3lame - (typically 'lame' in your system's package manager) 19 | * GNU grep - macOS or BSD users may need to install through package manager 20 | * GNU sed - see above 21 | * GNU find - see above 22 | * jq - only if `--use-audible-cli-data` is set or if converting an .aaxc file 23 | * mp4art used to add cover art to m4a and m4b files. Optional 24 | * mediainfo used to add additional media tags like narrator. Optional 25 | 26 | ## Usage(s) 27 | ``` 28 | 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 ] ... 29 | ``` 30 | or if you want to get guided through the options 31 | ``` 32 | bash interactiveAAXtoMP3 [-a|--advanced] [-h|--help] 33 | ``` 34 | 35 | * **<AAX INPUT_FILES>**... are considered input file(s), useful for batching! 36 | 37 | ## Options for AAXtoMP3 38 | * **-f** or **--flac** Flac Encoding and as default produces a single file. 39 | * **-o** or **--opus** Ogg/Opus Encoding defaults to multiple file output by chapter. The extension is .ogg 40 | * **-a** or **--aac** AAC Encoding and produce a m4a single files output. 41 | * **-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. 42 | * **-n** or **--no-clobber** If set and the target directory already exists, AAXtoMP3 will exit without overwriting anything. 43 | * **-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. 44 | * **-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. 45 | * **-V** or **--validate** Perform 2 validation tests on the supplied aax files. This is more extensive than the normal validation as we attempt to transcode the aax file to a null file. This can take a long period of time. However it is useful when inspecting a large set of aax files prior to transcoding. As download errors are common with Audible servers. 46 | * **-e:mp3** Identical to defaults. 47 | * **-e:m4a** Create a m4a audio file. This is identical to --aac 48 | * **-e:m4b** Create a m4b audio file. This is the book version of the m4a format. 49 | * **-s** or **--single** Output a single file for the entire book. If you only want a single ogg file for instance. 50 | * **-c** or **--chaptered** Output a single file per chapter. The `--chaptered` will only work if it follows the `--aac -e:m4a -e:m4b --flac` options. 51 | * **--continue <CHAPTERNUMBER>** If the splitting into chapters gets interrupted (e.g. by a weak battery on your laptop) you can go on where the process got interrupted. Just delete the last chapter (which was incompletely generated) and redo the task with "--continue <CHAPTERNUMBER>" where CHAPTERNUMBER is the chapter that got interrupted. 52 | * **--level <COMPRESSIONLEVEL>** Set compression level. May be given for mp3, flac and opus. 53 | * **--keep-author <FIELD>** If a book has multiple authors and you don't want all of them in the metadata, with this flag you can specify a specific author (1 is the first, 2 is the second...) to keep while discarding the others. 54 | * **--author <AUTHOR>** Manually set the author metadata field, useful if you have multiple books of the same author but the name reported is different (eg. spacing, accents..). Has precedence over `--keep-author`. 55 | * **-l** or **--loglevel <LOGLEVEL>** Set loglevel: 0 = progress only, 1 (default) = more information, output of chapter splitting progress is limitted to a progressbar, 2 = more information, especially on chapter splitting, 3 = debug mode 56 | * **--dir-naming-scheme <STRING>** or **-D** Use a custom directory naming scheme, with variables. See [below](#custom-naming-scheme) for more info. 57 | * **--file-naming-scheme <STRING>** or **-F** Use a custom file naming scheme, with variables. See [below](#custom-naming-scheme) for more info. 58 | * **--chapter-naming-scheme <STRING>** Use a custom chapter naming scheme, with variables. See [below](#custom-naming-scheme) for more info. 59 | * **--use-audible-cli-data** Use additional data got with mkb79/audible-cli. See [below](#audible-cli-integration) for more info. Needed for the files in the `aaxc` format. 60 | * **--audible-cli-library-file** or **-L** Path of the library-file, generated by mkb79/audible-cli (`audible library export -o ./library.tsv`). Only available if `--use-audible-cli-data` is set. This file is required to parse additional metadata such as `$series` or `$series_sequence`. 61 | * **--ffmpeg-path** Set the ffmpeg/ffprobe binaries folder. Both of them must be executable and in the same folder. 62 | * **--ffmpeg-name** Set a custom name for the ffmpeg binary. Must be executable and in path, or in custom path specified by --ffmpeg-path. 63 | * **--ffprobe-name** Set a custom name for the ffprobe binary. Must be executable and in path, or in custom path specified by --ffmpeg-path. 64 | 65 | ## Options for interactiveAAXtoMP3 66 | * **-a** or **--advanced** Get more options to choose. Not used right now. 67 | * **-h** or **--help** Get a help prompt. 68 | This script presents you the options you chose last time as default. 69 | When you get asked for the aax-file you may just drag'n'drop it to the terminal. 70 | 71 | ### AUTHCODE 72 | **Your** Audible auth code (it won't correctly decode otherwise) (not required to decode the `aaxc` format). 73 | 74 | #### Determining your own AUTHCODE 75 | You will need your authentication code that comes from Audible's servers. This 76 | will be used by ffmpeg to perform the initial audio convert. You can obtain 77 | this string from a tool like 78 | [audible-activator](https://github.com/inAudible-NG/audible-activator) or like [audible-cli](https://github.com/mkb79/audible-cli). 79 | 80 | #### Specifying the AUTHCODE. 81 | In order of __precidence__. 82 | 1. __--authcode [AUTHCODE]__ The command line option. With the highest precedence. 83 | 2. __.authcode__ If this file is placed in the current working directory and contains only the authcode it is used if the above is not. 84 | 3. __~/.authcode__ a global config file for all the tools. And is used as the default if none of the above are specified. 85 | __Note:__ At least one of the above must be exist if converting `aax` files. The code must also match the encoding for the user that owns the AAX file(s). If the authcode does not match the AAX file no transcoding will occur. 86 | 87 | ### MP3 Encoding 88 | * This is the **default** encoding 89 | * Produces 1 or more mp3 files for the AAX title. 90 | * The default mode is **chaptered** 91 | * If you want a mp3 file per chapter do not use the **--single** option. 92 | * A m3u playlist file will also be created in this instance in the case of **default** chaptered output. 93 | * **--level** has to be in range 0-9, where 9 is fastest and 0 is highest quality. Please note: The quality can **never** become higher than the qualitiy of the original aax file! 94 | 95 | ### Ogg/Opus Encoding 96 | * Can be done by using the **-o** or **--opus** command line switches 97 | * The default mode is **chaptered** 98 | * Opus coded files are stored in the ogg container format for better compatibility. 99 | * **--level** has to be in range 0-10, where 0 is fastest and 10 is highest quality. Please note: The quality can **never** become higher than the qualitiy of the original aax file! 100 | 101 | ### AAC Encoding 102 | * Can be done by using the **-a** or **--aac** command line switches 103 | * The default mode is **single** 104 | * Designed to be the successor of the MP3 format 105 | * Generally achieves better sound quality than MP3 at the same bit rate. 106 | * This will only produce 1 audio file as output. 107 | 108 | ### FLAC Encoding 109 | * Can be done by using the **-f** or **--flac** command line switches 110 | * The default mode is **single** 111 | * FLAC is an open format with royalty-free licensing 112 | * This will only produce 1 audio file as output. If you want a flac file per chapter do use **-c** or **--chaptered**. 113 | * **--level** has to be in range 0-12, where 0 is fastest and 12 is highest compression. Since flac is lossless, the quality always remains the same. 114 | 115 | ### M4A and M4B Containers 116 | * These containers were created by Apple Inc. They were meant to be the successor to mp3. 117 | * M4A is a container that is meant to hold music and is typically of a higher bitrate. 118 | * M4B is a container that is meant to hold audiobooks and is typically has bitrates of 64k and 32k. 119 | * Both formats are chaptered 120 | * Both support coverart internal 121 | * The default mode is **single** 122 | 123 | ### Validating AAX files 124 | * The **--validate** option will result in only a validation pass over the supplied aax file(s). No transcoding will occur. This is useful when you wish to ensure you have a proper download of your personal Audible audio books. With this option all supplied books are validated. 125 | * If you do NOT supply the **--validate** option all audio books are still validated when they are processed. However if there is an invalid audio book in the supplied list of books the processing will stop at that point. 126 | * A third test is performed on the file where the entire file is inspected to see if it is valid. This is a lengthy process. However it will not break the script when an invalid file is found. 127 | * The 3 test current are: 128 | 1. aax present 129 | 1. meta data header in file is valid and complete 130 | 1. entire file is valid and complete. _only executed with the **--validate** option._ 131 | 132 | ### Defaults 133 | * Default out put directory is the base directory of each file listed. Plus the genre, Artist and Title of the Audio Book. 134 | * The default codec is mp3 135 | * The default output is by chapter. 136 | 137 | ### Custom naming scheme 138 | The following flags can modify the default naming scheme: 139 | * **--dir-naming-scheme** or **-D** 140 | * **--file-naming-scheme** or **-F** 141 | * **--chapter-naming-scheme** 142 | 143 | Each flag takes a string as argument. If the string contains a variable defined in the script (eg. artist, title, chapter, narrator...), the corresponding value is used. 144 | The default options correspond to the following flags: 145 | * `--dir-naming-scheme '$genre/$artist/$title'` 146 | * `--file-naming-scheme '$title'` 147 | * `--chapter-naming-scheme '$title-$(printf %0${#chaptercount}d $chapternum) $chapter'` 148 | 149 | Additional notes: 150 | * If a command substitution is present in the passed string, (for example `$(printf %0${#chaptercount}d $chapternum)`, used to pad with zeros the chapter number), the commands are executed. 151 | So you can use `--dir-naming-scheme '$(date +%Y)/$artist'`, but using `--file-naming-scheme '$(rm -rf /)'` is a really bad idea. Be careful. 152 | * You can use basic text, like `--dir-naming-scheme 'Converted/$title'` 153 | * You can also use shell variables as long as you escape them properly: `CustomGenre=Horror ./AAXtoMP3 --dir-naming-scheme "$CustomGenre/\$artist/\$title" *.aax` 154 | * If you want shorter chapter names, use `--chapter-naming-scheme '$(printf %0${#chaptercount}d $chapternum) $chapter'`: only chapter number and chapter name 155 | * If you want to append the narrator name to the title, use `--dir-naming-scheme '$genre/$artist/$title-$narrator' --file-naming-scheme '$title-$narrator'` 156 | * If you don't want to have the books separated by author, use `--dir-naming-scheme '$genre/$title'` 157 | * To be able to use `$series` or `$series_sequence` in the schemes the following is required: 158 | * `--use-audible-cli-data` is set 159 | * you have pre-generated the library-file via `audible library export -o ./library.tsv` 160 | * you have set the path to the generated library-file via `--audible-cli-library-file ./library.tsv` 161 | 162 | ### Installing Dependencies. 163 | In general, take a look at [command-not-found.com](https://command-not-found.com/) 164 | #### FFMPEG,FFPROBE 165 | __Ubuntu, Linux Mint, Debian__ 166 | ``` 167 | sudo apt-get update 168 | sudo apt-get install ffmpeg libav-tools x264 x265 bc 169 | ``` 170 | 171 | In Debian-based system's repositories the ffmpeg version is often outdated. If you want 172 | to convert .aaxc files, you need at least ffmpeg 4.4. So if your installed version 173 | needs to be updated, you can either install a custom repository that has the newer version, 174 | compile ffmpeg from source or download pre-compiled binaries. 175 | You can then tell AAXtoMP3 to use the compiled binaries with the `--ffmpeg-path` flag. 176 | You need to specify the folder where the ffmpeg and ffprobe binaries are. Make sure 177 | they are both executable. 178 | 179 | If you have snapd installed, you can also install a recent version of 4.4 from the edge channel: 180 | ``` 181 | snap install ffmpeg --edge 182 | ``` 183 | In this case you will need to confiure a custom path _and_ binary name for ffprobe, `--ffmpeg-path /snap/bin/ --ffprobe-name ffmpeg.ffprobe`. 184 | 185 | __Fedora__ 186 | 187 | Fedora users need to enable the rpm fusion repository to install ffmpeg. Version 22 and upwards are currently supported. The following command works independent of your current version: 188 | ``` 189 | sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm 190 | ``` 191 | Afterwards use the package manager to install ffmpeg: 192 | ``` 193 | sudo dnf install ffmpeg 194 | ``` 195 | 196 | __RHEL or compatible like CentOS__ 197 | 198 | RHEL version 6 and 7 are currently able to use rpm fusion. 199 | In order to use rpm fusion you have to enable EPEL, see http://fedoraproject.org/wiki/EPEL 200 | 201 | Add the rpm fusion repositories in version 6 202 | ``` 203 | sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-6.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-6.noarch.rpm 204 | ``` 205 | or version 7: 206 | ``` 207 | sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-7.noarch.rpm 208 | ``` 209 | then install ffmpeg: 210 | ``` 211 | sudo yum install ffmpeg 212 | ``` 213 | 214 | __MacOS__ 215 | ``` 216 | brew install ffmpeg 217 | brew install gnu-sed 218 | brew install grep 219 | brew install findutils 220 | ``` 221 | 222 | #### mp4art/mp4chaps 223 | _Note: This is an optional dependency, required for adding cover art to m4a and b4b files only._ 224 | 225 | __Ubuntu, Linux Mint, Debian__ 226 | ``` 227 | sudo apt-get update 228 | sudo apt-get install mp4v2-utils 229 | ``` 230 | 231 | On Debian and Ubuntu the mp4v2-utils package has been deprecated and removed, as the upsteam project is no longer maintained. 232 | The package was removed in Debian Buster, and Ubuntu Focal [ 20.04 ]. 233 | 234 | __CentOS, RHEL & Fedora__ 235 | ``` 236 | # CentOS/RHEL and Fedora users make sure that you have enabled atrpms repository in system. Let’s begin installing FFmpeg as per your operating system. 237 | yum install mp4v2-utils 238 | ``` 239 | __MacOS__ 240 | ``` 241 | brew install mp4v2 242 | ``` 243 | 244 | #### mediainfo 245 | _Note: This is an optional dependency._ 246 | 247 | __Ubuntu, Linux Mint, Debian__ 248 | ``` 249 | sudo apt-get update 250 | sudo apt-get install mediainfo 251 | ``` 252 | __CentOS, RHEL & Fedora__ 253 | ``` 254 | yum install mediainfo 255 | ``` 256 | __MacOS__ 257 | ``` 258 | brew install mediainfo 259 | ``` 260 | ## AAXC files 261 | The AAXC format is a new Audible encryption format, meant to replace the old AAX. 262 | The encryption has been updated, and now to decrypt the file the authcode 263 | is not sufficient, we need two "keys" which are unique for each audiobook. 264 | Since getting those keys is not simple, for now the method used to get them 265 | is handled by the package audible-cli, that stores 266 | them in a file when downloading the aaxc file. This means that in order to 267 | decrypt the aaxc files, they must be downloaded with audible-cli. 268 | Note that you need at least [ffmpeg 4.4](#ffmpegffprobe). 269 | 270 | ## Audible-cli integration 271 | Some information are not present in the AAX file. For example the chapters's 272 | title, additional chapters division (Opening and End credits, Copyright and 273 | more). Those information are avaiable via a non-public audible API. This 274 | [repo](https://github.com/mkb79/Audible) provides a python API wrapper, and the 275 | [audible-cli](https://github.com/mkb79/audible-cli) packege makes easy to get 276 | more info. In particular the flags **--cover --cover-size 1215 --chapter** 277 | downloads a better-quality cover (.jpg) and detailed chapter infos (.json). 278 | More info are avaiable on the package page. 279 | 280 | Some books might not be avaiable in the old `aax` format, but only in the newer 281 | `aaxc` format. In that case, you can use [audible-cli](https://github.com/mkb79/audible-cli) 282 | to download them. For example, to download all the books in your library in the newer `aaxc` format, as well as 283 | chapters's title and an HQ cover: `audible download --all --aaxc --cover --cover-size 1215 --chapter`. 284 | 285 | To make AAXtoMP3 use the additional data, specify the **--use-audible-cli-data** 286 | flag: it expects the cover and the chapter files (and the voucher, if converting 287 | an aaxc file) to be in the same location of the AAX file. The naming of these 288 | files must be the one set by audible-cli. When converting aaxc files, the variable 289 | is automatically set, so be sure to follow the instructions in this paragraph. 290 | 291 | For more information on how to use the `audible-cli` package, check out the git page [audible-cli](https://github.com/mkb79/audible-cli). 292 | 293 | Please note that right now audible-cli is in dev stage, so keep in mind that the 294 | naming scheme of the additional files, the flags syntax and other things can 295 | change without warning. 296 | 297 | 298 | ## Anti-Piracy Notice 299 | Note that this project **does NOT ‘crack’** the DRM. It simply allows the user to 300 | use their own encryption key (fetched from Audible servers) to decrypt the 301 | audiobook in the same manner that the official audiobook playing software does. 302 | 303 | Please only use this application for gaining full access to your own audiobooks 304 | for archiving/conversion/convenience. DeDRMed audiobooks should not be uploaded 305 | to open servers, torrents, or other methods of mass distribution. No help will 306 | be given to people doing such things. Authors, retailers, and publishers all 307 | need to make a living, so that they can continue to produce audiobooks for us to 308 | hear, and enjoy. Don’t be a parasite. 309 | 310 | This blurb is borrowed from the https://apprenticealf.wordpress.com/ page. 311 | 312 | ## License 313 | Changed the license to the WTFPL, do whatever you like with this script. Ultimately it's just a front-end for ffmpeg after all. 314 | 315 | ## Need Help? 316 | I'll help out if you are having issues, just submit and issue and I'll get back to you when I can. 317 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /interactiveAAXtoMP3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ===Note for contributors======================================================================================================================== 4 | 5 | # This script interactively asks the user for the options to call AAXtoMP3 with. This first version does not include all options of AAXtoMP3 6 | # since I tried to keep the dialog short, but I added an --advanced option, which is unused right now, but might be used in the future to add 7 | # more options which only show up if explicitely wanted. 8 | # If you want to add functionality please consider, whether the functionality you add might belong to the advanced options. 9 | 10 | # ===Variables==================================================================================================================================== 11 | 12 | # Help message 13 | help=$'\nUsage: interactiveAAXtoMP3 [--advanced] [--help]\n 14 | --advanced More options 15 | --help Print this message\n' 16 | summary="" # This will contain a summary of the options allready set. 17 | call="./AAXtoMP3" # This will contain the call for AAXtoMP3. 18 | advanced=0 # Toggles advanced options on or off. 19 | 20 | # ===Options====================================================================================================================================== 21 | 22 | while true; do 23 | case "$1" in 24 | # Advanced options. 25 | -a | --advanced ) advanced=1; shift ;; 26 | # Command synopsis. 27 | -h | --help ) echo -e "$help"; exit ;; 28 | # Anything else stops command line processing. 29 | * ) break ;; 30 | esac 31 | done 32 | 33 | # ===Cross platform compatible use grep and sed=================================================================================================== 34 | 35 | # ===Detect which annoying version of grep we have=== 36 | GREP=$(grep --version | grep -q GNU && echo "grep" || echo "ggrep") 37 | if ! [[ $(type -P "$GREP") ]]; then 38 | echo "$GREP (GNU grep) is not in your PATH" 39 | echo "Without it, this script will break." 40 | echo "On macOS, you may want to try: brew install grep" 41 | exit 1 42 | fi 43 | 44 | # ===Detect which annoying version of sed we have=== 45 | SED=$(sed --version 2>&1 | $GREP -q GNU && echo "sed" || echo "gsed") 46 | if ! [[ $(type -P "$SED") ]]; then 47 | echo "$SED (GNU sed) is not in your PATH" 48 | echo "Without it, this script will break." 49 | echo "On macOS, you may want to try: brew install gnu-sed" 50 | exit 1 51 | fi 52 | 53 | # ===Get options from last time=================================================================================================================== 54 | 55 | # ===Set default values=== 56 | lastcodec="mp3" 57 | lastcompression="4" 58 | lastchapters="yes" 59 | lastauthcode="" 60 | lastloglevel="1" 61 | 62 | # ===Get Values from last time=== 63 | if [ -f ".interactivesave" ]; then 64 | for ((i=1;i<=$(wc -l .interactivesave | cut -d " " -f 1);i++)) do 65 | line=$(head -$i .interactivesave | tail -1) 66 | case $(echo $line | cut -d " " -f 1 | $SED 's/.$//') in 67 | codec ) lastcodec="$(echo $line | cut -d " " -f 2)";; 68 | compression ) lastcompression="$(echo $line | cut -d " " -f 2)";; 69 | chapters ) lastchapters="$(echo $line | cut -d " " -f 2)";; 70 | authcode ) lastauthcode="$(echo $line | cut -d " " -f 2)";; 71 | loglevel ) lastloglevel="$(echo $line | cut -d " " -f 2)";; 72 | * ) rm .interactivesave; exit 1;; 73 | esac 74 | done 75 | fi 76 | 77 | # ===Get options for AAXtoMP3===================================================================================================================== 78 | 79 | # ===Codec=== 80 | while true; do 81 | clear; 82 | read -e -p "codec (mp3/m4a/m4b/flac/aac/opus): " -i "$lastcodec" codec 83 | case "$codec" in 84 | mp3 ) summary="$summary""codec: $codec"; call="$call -e:mp3"; break;; 85 | m4a ) summary="$summary""codec: $codec"; call="$call -e:m4a"; break;; 86 | m4b ) summary="$summary""codec: $codec"; call="$call -e:m4b"; break;; 87 | flac ) summary="$summary""codec: $codec"; call="$call --flac"; break;; 88 | aac ) summary="$summary""codec: $codec"; call="$call --aac"; break;; 89 | opus ) summary="$summary""codec: $codec"; call="$call --opus"; break;; 90 | esac 91 | done 92 | 93 | # ===Compression=== 94 | while true; do 95 | clear; echo -e "$summary" 96 | case "$codec" in 97 | mp3 ) maxlevel=9;; 98 | flac ) maxlevel=12;; 99 | opus ) maxlevel=10;; 100 | * ) break;; 101 | esac 102 | read -e -p "compression level (0-$maxlevel): " -i "$lastcompression" compression 103 | if [[ $compression =~ ^[0-9]+$ ]] && [[ "$compression" -ge "0" ]] && [[ "$compression" -le "$maxlevel" ]]; then 104 | summary="$summary""\ncompression level: $compression" 105 | call="$call --level $compression" 106 | break 107 | fi 108 | done 109 | 110 | # ===Chapters=== 111 | while true; do 112 | clear; echo -e "$summary" 113 | read -e -p "chapters (yes/no/chapternumber to continue with): " -i "$lastchapters" chapters 114 | case "$chapters" in 115 | ^[0-9]+$ ) summary="$summary""\nchapters: $chapters"; call="$call -c --continue ${chapters}"; break;; 116 | yes ) summary="$summary""\nchapters: $chapters"; call="$call -c"; break;; 117 | no ) summary="$summary""\nchapters: $chapters"; call="$call -s"; break;; 118 | esac 119 | done 120 | 121 | # ===Authcode=== 122 | if ! [ -r .authcode ] || [ -r ~/.authcode ]; then 123 | clear; echo -e "$summary" 124 | read -e -p "Authcode: " -i "$lastauthcode" authcode 125 | summary="$summary""\nauthcode: $authcode" 126 | call="$call -A $authcode" 127 | fi 128 | 129 | # ===Loglevel=== 130 | while true; do 131 | clear; echo -e "$summary" 132 | read -e -p "loglevel (0/1/2/3): " -i "$lastloglevel" loglevel 133 | if [[ $loglevel =~ ^[0-9]+$ ]] && [[ "$loglevel" -ge "0" ]] && [[ "$loglevel" -le "3" ]]; then 134 | summary="$summary""\nloglevel: $loglevel" 135 | call="$call -l $loglevel" 136 | break 137 | fi 138 | done 139 | 140 | # ===File=== 141 | clear; echo -e "$summary" 142 | read -p "aax-file: " file 143 | file="${file%\'}" #remove suffix ' if file is given via drag'n'drop 144 | file="${file#\'}" #remove prefix ' if file is given via drag'n'drop 145 | savefile="$summary" 146 | summary="$summary""\naax-file: $file" 147 | call="$call $(echo $file | $SED "s;~;$HOME;")" 148 | 149 | # ===Summerize chosen options and call AAXtoMP3=================================================================================================== 150 | 151 | # ===Summary=== 152 | clear; echo -e "$summary\n" 153 | echo -e "$call\n" 154 | 155 | # ===Save chosen options=== 156 | echo -e $savefile | $SED "s;\ level:;:;" > .interactivesave 157 | 158 | # ===Call AAXtoMP3=== 159 | $call 160 | --------------------------------------------------------------------------------