diff --git a/AAXtoMP3 b/AAXtoMP3 index adc91ef..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 ] -[--use-audible-cli-data] [--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 @@ -34,6 +34,10 @@ 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 @@ -44,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. @@ -55,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. @@ -82,12 +84,20 @@ while true; do --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. @@ -212,6 +222,16 @@ else 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 @@ -242,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:" @@ -255,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:" @@ -402,7 +422,7 @@ validate_aax() { set +e errexit # Take a look at the aax file and see if it is valid. If the source file is aaxc, we give ffprobe additional flags - output="$(ffprobe -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)" + output="$("$FFPROBE" -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)" # If invalid then say something. if [[ $? != "0" ]] ; then @@ -415,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 ${decrypt_param} -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 @@ -477,6 +497,15 @@ validate_extra_files() { 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" } @@ -485,7 +514,7 @@ validate_extra_files() { 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() @@ -507,9 +536,42 @@ save_metadata() { # we use some characters (#) as placeholder, add some new lines, # put a ',' after the start value, we calculate the end of each chapter # as start+length, and we convert (divide) the time stamps from ms to s. - # Then we delete all ':' since they make a filename invalid. + # 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}" \ - | tr -d ':' >> "$metadata_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" @@ -617,6 +679,8 @@ do 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 @@ -639,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 @@ -654,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}" @@ -674,7 +739,7 @@ do # Display the total length of the audiobook in format hh:mm:ss # 10#$var force base-10 interpretation. By default it's base-8, so values like 08 or 09 are not octal numbers - total_length="$(ffprobe -v error ${decrypt_param} -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${aax_file}" | cut -d . -f 1)" + 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))" @@ -693,8 +758,8 @@ 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 ${decrypt_param} -i "${aax_file}" -vn -codec:a "${codec}" -ab ${bitrate} -map_metadata -1 -metadata title="${title}" -metadata artist="${artist}" -metadata album_artist="${album_artist}" -metadata album="${album}" -metadata date="${album_date}" -metadata track="1/1" -metadata genre="${genre}" -metadata copyright="${copyright}" "${output_file}"' - 0))" == "1" ]; then @@ -722,8 +789,7 @@ do # ----- fi # Grab the cover art if available. - cover_file="${output_directory}/cover.jpg" - extra_crop_cover='' + cover_file="${output_directory}/${currentFileNameScheme}.jpg" if [ "${continue}" == "0" ]; then if [ "${audibleCli}" == "1" ]; then # We have a better quality cover file, copy it. @@ -731,21 +797,27 @@ do log "Copy cover file to ${cover_file}..." fi cp "${extra_cover_file}" "${cover_file}" - - # We now set a variable, ${extra_crop_cover}, which contains an additional - # ffmpeg flag. It crops the cover so the width and the height is divisible by two. - # Since the standard (in the aax file) image resolution is 512, we set the flag - # only if we use a custom cover art. - extra_crop_cover='-vf crop=trunc(iw/2)*2:trunc(ih/2)*2' else # Audible-cli not used, extract the cover from the aax file if [ "$((${loglevel} > 1))" == "1" ]; then log "Extracting cover into ${cover_file}..." fi - &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 + # ----- # 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) @@ -823,7 +895,7 @@ do #ffmpeg version 4+ and on the output for all older versions. split_input="" split_output="" - if [ "$(($(ffmpeg -version | $SED -E 's/[^0-9]*([0-9]).*/\1/g;1q') > 3))" = "1" ]; then + 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}" @@ -837,7 +909,7 @@ do 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}/${currentFileNameScheme}.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 48be47c..ee45eda 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Audible fails for some reason. ## 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] [--use-audible-cli-data]] [-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 ``` @@ -57,6 +57,10 @@ bash interactiveAAXtoMP3 [-a|--advanced] [-h|--help] * **--file-naming-scheme <STRING>** or **-F** Use a custom file naming scheme, with variables. See [below](#custom-naming-scheme) for more info. * **--chapter-naming-scheme <STRING>** Use a custom chapter naming scheme, with variables. See [below](#custom-naming-scheme) for more info. * **--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. +* **--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`. +* **--ffmpeg-path** Set the ffmpeg/ffprobe binaries folder. Both of them must be executable and in the same folder. +* **--ffmpeg-name** Set a custom name for the ffmpeg binary. Must be executable and in path, or in custom path specified by --ffmpeg-path. +* **--ffprobe-name** Set a custom name for the ffprobe binary. Must be executable and in path, or in custom path specified by --ffmpeg-path. ## Options for interactiveAAXtoMP3 * **-a** or **--advanced** Get more options to choose. Not used right now. @@ -150,6 +154,10 @@ So you can use `--dir-naming-scheme '$(date +%Y)/$artist'`, but using `--file-na * If you want shorter chapter names, use `--chapter-naming-scheme '$(printf %0${#chaptercount}d $chapternum) $chapter'`: only chapter number and chapter name * If you want to append the narrator name to the title, use `--dir-naming-scheme '$genre/$artist/$title-$narrator' --file-naming-scheme '$title-$narrator'` * If you don't want to have the books separated by author, use `--dir-naming-scheme '$genre/$title'` +* To be able to use `$series` or `$series_sequence` in the schemes the following is required: + * `--use-audible-cli-data` is set + * you have pre-generated the library-file via `audible library export -o ./library.tsv` + * you have set the path to the generated library-file via `--audible-cli-library-file ./library.tsv` ### Installing Dependencies. In general, take a look at [command-not-found.com](https://command-not-found.com/) @@ -160,6 +168,20 @@ sudo apt-get update sudo apt-get install ffmpeg libav-tools x264 x265 bc ``` +In Debian-based system's repositories the ffmpeg version is often outdated. If you want +to convert .aaxc files, you need at least ffmpeg 4.4. So if your installed version +needs to be updated, you can either install a custom repository that has the newer version, +compile ffmpeg from source or download pre-compiled binaries. +You can then tell AAXtoMP3 to use the compiled binaries with the `--ffmpeg-path` flag. +You need to specify the folder where the ffmpeg and ffprobe binaries are. Make sure +they are both executable. + +If you have snapd installed, you can also install a recent version of 4.4 from the edge channel: +``` +snap install ffmpeg --edge +``` +In this case you will need to confiure a custom path _and_ binary name for ffprobe, `--ffmpeg-path /snap/bin/ --ffprobe-name ffmpeg.ffprobe`. + __Fedora__ 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: @@ -198,13 +220,17 @@ brew install findutils ``` #### mp4art/mp4chaps -_Note: This is an optional dependency._ +_Note: This is an optional dependency, required for adding cover art to m4a and b4b files only._ __Ubuntu, Linux Mint, Debian__ ``` sudo apt-get update sudo apt-get install mp4v2-utils ``` + +On Debian and Ubuntu the mp4v2-utils package has been deprecated and removed, as the upsteam project is no longer maintained. +The package was removed in Debian Buster, and Ubuntu Focal [ 20.04 ]. + __CentOS, RHEL & Fedora__ ``` # 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. @@ -239,6 +265,7 @@ Since getting those keys is not simple, for now the method used to get them is handled by the package audible-cli, that stores them in a file when downloading the aaxc file. This means that in order to decrypt the aaxc files, they must be downloaded with audible-cli. +Note that you need at least [ffmpeg 4.4](#ffmpegffprobe). ## Audible-cli integration Some information are not present in the AAX file. For example the chapters's