forked from KrumpetPirate/AAXtoMP3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAAXtoMP3
executable file
·1014 lines (920 loc) · 41.7 KB
/
AAXtoMP3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# ========================================================================
# Command Line Options
# Usage Synopsis.
usage=$'\nUsage: AAXtoMP3 [--flac] [--aac] [--opus ] [--single] [--level <COMPRESSIONLEVEL>]
[--chaptered] [-e:mp3] [-e:m4a] [-e:m4b] [--authcode <AUTHCODE>] [--no-clobber]
[--target_dir <PATH>] [--complete_dir <PATH>] [--validate] [--loglevel <LOGLEVEL>]
[--keep-author <N>] [--author <AUTHOR>] [--{dir,file,chapter}-naming-scheme <STRING>]
[--use-audible-cli-data] [--audible-cli-library-file <LIBRARY_PATH>] [--continue <CHAPTERNUMBER>] {FILES}\n'
codec=libmp3lame # Default encoder.
extension=mp3 # Default encoder extension.
level=-1 # Compression level. Can be given for mp3, flac and opus. -1 = default/not specified.
mode=chaptered # Multi file output
auth_code= # Required to be set via file or option.
targetdir= # Optional output location. Note default is basedir of AAX file.
dirNameScheme= # Custom directory naming scheme, default is $genre/$artist/$title
customDNS=0
fileNameScheme= # Custom file naming scheme, default is $title
customFNS=0
chapterNameScheme= # Custom chapter naming scheme, default is '$title-$(printf %0${#chaptercount}d $chapternum) $chapter' (BookTitle-01 Chapter 1)
customCNS=0
completedir= # Optional location to move aax files once the decoding is complete.
container=mp3 # Just in case we need to change the container. Used for M4A to M4B
VALIDATE=0 # Validate the input aax file(s) only. No Transcoding of files will occur
loglevel=1 # Loglevel: 0: Show progress only; 1: default; 2: a little more information, timestamps; 3: debug
noclobber=0 # Default off, clobber only if flag is enabled
continue=0 # Default off, If set Transcoding is skipped and chapter splitting starts at chapter continueAt.
continueAt=1 # Optional chapter to continue splitting the chapters.
keepArtist=-1 # Default off, if set change author metadata to use the passed argument as field
authorOverride= # Override the author, ignoring the metadata
audibleCli=0 # Default off, Use additional data gathered from mkb79/audible-cli
aaxc_key= # Initialize variables, in case we need them in debug_vars
aaxc_iv= # Initialize variables, in case we need them in debug_vars
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
# the $1 will no longer be a ARGV element. So you should only do basic variable setting above here.
#
# Process the command line options. This allows for un-ordered options. Sorta like a getops style
while true; do
case "$1" in
# Flac encoding
-f | --flac ) codec=flac; extension=flac; mode=single; container=flac; shift ;;
# Ogg Format
-o | --opus ) codec=libopus; extension=opus; container=ogg; shift ;;
# If appropriate use only a single file output.
-s | --single ) mode=single; shift ;;
# If appropriate use only a single file output.
-c | --chaptered ) mode=chaptered; shift ;;
# This is the same as --single option.
-e:mp3 ) codec=libmp3lame; extension=mp3; mode=single; container=mp3; shift ;;
# Identical to --acc option.
-e:m4a | -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.
-t | --target_dir ) targetdir="$2"; shift 2 ;;
# Use a custom directory naming scheme, with variables.
-D | --dir-naming-scheme ) dirNameScheme="$2"; customDNS=1; shift 2 ;;
# Use a custom file naming scheme, with variables.
-F | --file-naming-scheme ) fileNameScheme="$2"; customFNS=1; shift 2 ;;
# Use a custom chapter naming scheme, with variables.
--chapter-naming-scheme ) chapterNameScheme="$2"; customCNS=1; shift 2 ;;
# Move the AAX file to a new directory when decoding is complete.
-C | --complete_dir ) completedir="$2"; shift 2 ;;
# Authorization code associate with the AAX file(s)
-A | --authcode ) auth_code="$2"; shift 2 ;;
# Don't overwrite the target directory if it already exists
-n | --no-clobber ) noclobber=1; shift ;;
# Extremely verbose output.
-d | --debug ) loglevel=3; shift ;;
# Set loglevel.
-l | --loglevel ) loglevel="$2"; shift 2 ;;
# Validate ONLY the aax file(s) No transcoding occurs
-V | --validate ) VALIDATE=1; shift ;;
# continue splitting chapters at chapter continueAt
--continue ) continueAt="$2"; continue=1; shift 2 ;;
# Use additional data got with mkb79/audible-cli
--use-audible-cli-data ) audibleCli=1; shift ;;
# 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.
-- ) shift; break ;;
# Anything else stops command line processing.
* ) break ;;
esac
done
# -----
# Empty argv means we have nothing to do so lets bark some help.
if [ "$#" -eq 0 ]; then
printf "$usage" $0
exit 1
fi
# Setup safer bash script defaults.
set -o errexit -o noclobber -o nounset -o pipefail
# ========================================================================
# Utility Functions
# -----
# debug
# debug "Some longish message"
debug() {
if [ $loglevel == 3 ] ; then
echo "$(date "+%F %T%z") DEBUG ${1}"
fi
}
# -----
# debug dump contents of a file to STDOUT
# debug "<full path to file>"
debug_file() {
if [ $loglevel == 3 ] ; then
echo "$(date "+%F %T%z") DEBUG"
echo "=Start=========================================================================="
cat "${1}"
echo "=End============================================================================"
fi
}
# -----
# debug dump a list of internal script variables to STDOUT
# debug_vars "Some Message" var1 var2 var3 var4 var5
debug_vars() {
if [ $loglevel == 3 ] ; then
msg="$1"; shift ; # Grab the message
args=("$@") # Grab the rest of the args
# determine the length of the longest key
l=0
for (( n=0; n<${#args[@]}; n++ )) ; do
(( "${#args[$n]}" > "$l" )) && l=${#args[$n]}
done
# Print the Debug Message
echo "$(date "+%F %T%z") DEBUG ${msg}"
echo "=Start=========================================================================="
# Using the max length of a var name we dynamically create the format.
fmt="%-"${l}"s = %s\n"
for (( n=0; n<${#args[@]}; n++ )) ; do
eval val="\$${args[$n]}" ; # We save off the value of the var in question for ease of coding.
echo "$(printf "${fmt}" ${args[$n]} "${val}" )"
done
echo "=End============================================================================"
fi
}
# -----
# log
log() {
if [ "$((${loglevel} > 1))" == "1" ] ; then
echo "$(date "+%F %T%z") ${1}"
else
echo "${1}"
fi
}
# -----
#progressbar produces a progressbar in the style of
# process: |####### | XX% (part/total unit)
# which is gonna be overwritten by the next line.
progressbar() {
#get input
part=${1}
total=${2}
#compute percentage and make print_percentage the same length regardless of the number of digits.
percentage=$((part*100/total))
if [ "$((percentage<10))" = "1" ]; then print_percentage=" $percentage"
elif [ "$((percentage<100))" = "1" ]; then print_percentage=" $percentage"
else print_percentage="$percentage"; fi
#draw progressbar with one # for every 5% and blank spaces for the missing part.
progressbar=""
for (( n=0; n<(percentage/5); n++ )) ; do progressbar="$progressbar#"; done
for (( n=0; n<(20-(percentage/5)); n++ )) ; do progressbar="$progressbar "; done
#print progressbar
echo -ne "Chapter splitting: |$progressbar| $print_percentage% ($part/$total chapters)\r"
}
# Print out what we have already after command line processing.
debug_vars "Command line options as set" codec extension mode container targetdir completedir auth_code keepArtist authorOverride audibleCli
# ========================================================================
# Variable validation
if [ $(uname) = 'Linux' ]; then
GREP="grep"
FIND="find"
SED="sed"
else
GREP="ggrep"
FIND="gfind"
SED="gsed"
fi
# 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
if ! [[ $(type -P "$GREP") ]]; then
echo "$GREP (GNU grep) is not in your PATH"
echo "Without it, this script will break."
echo "On macOS, you may want to try: brew install grep"
exit 1
fi
# -----
# Detect which annoying version of find we have
if ! [[ $(type -P "$FIND") ]]; then
echo "$FIND (GNU find) is not in your PATH"
echo "Without it, this script will break."
echo "On macOS, you may want to try: brew install findutils"
exit 1
fi
# -----
# Detect which annoying version of sed we have
if ! [[ $(type -P "$SED") ]]; then
echo "$SED (GNU sed) is not in your PATH"
echo "Without it, this script will break."
echo "On macOS, you may want to try: brew install gnu-sed"
exit 1
fi
# -----
# Detect ffmpeg and ffprobe
if [[ "x$(type -P "$FFMPEG")" == "x" ]]; then
echo "ERROR ffmpeg was not found on your env PATH variable"
echo "Without it, this script will break."
echo "INSTALL:"
echo "MacOS: brew install ffmpeg"
echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc"
echo "Ubuntu (20.04): sudo apt-get update; sudo apt-get install ffmpeg x264 x265 bc"
echo "RHEL: yum install ffmpeg"
exit 1
fi
# -----
# Detect ffmpeg and ffprobe
if [[ "x$(type -P "$FFPROBE")" == "x" ]]; then
echo "ERROR ffprobe was not found on your env PATH variable"
echo "Without it, this script will break."
echo "INSTALL:"
echo "MacOS: brew install ffmpeg"
echo "Ubuntu: sudo apt-get update; sudo apt-get install ffmpeg libav-tools x264 x265 bc"
echo "RHEL: yum install ffmpeg"
exit 1
fi
# -----
# Detect if we need mp4art for cover additions to m4a & m4b files.
if [[ "x${container}" == "xmp4" && "x$(type -P mp4art)" == "x" ]]; then
echo "WARN mp4art was not found on your env PATH variable"
echo "Without it, this script will not be able to add cover art to"
echo "m4b files. Note if there are no other errors the AAXtoMP3 will"
echo "continue. However no cover art will be added to the output."
echo "INSTALL:"
echo "MacOS: brew install mp4v2"
echo "Ubuntu: sudo apt-get install mp4v2-utils"
fi
# -----
# Detect if we need mp4chaps for adding chapters to m4a & m4b files.
if [[ "x${container}" == "xmp4" && "x$(type -P mp4chaps)" == "x" ]]; then
echo "WARN mp4chaps was not found on your env PATH variable"
echo "Without it, this script will not be able to add chapters to"
echo "m4a/b files. Note if there are no other errors the AAXtoMP3 will"
echo "continue. However no chapter data will be added to the output."
echo "INSTALL:"
echo "MacOS: brew install mp4v2"
echo "Ubuntu: sudo apt-get install mp4v2-utils"
fi
# -----
# Detect if we need mediainfo for adding description and narrator
if [[ "x$(type -P mediainfo)" == "x" ]]; then
echo "WARN mediainfo was not found on your env PATH variable"
echo "Without it, this script will not be able to add the narrator"
echo "and description tags. Note if there are no other errors the AAXtoMP3"
echo "will continue. However no such tags will be added to the output."
echo "INSTALL:"
echo "MacOS: brew install mediainfo"
echo "Ubuntu: sudo apt-get install mediainfo"
fi
# -----
# Obtain the authcode from either the command line, local directory or home directory.
# See Readme.md for details on how to acquire your personal authcode for your personal
# audible AAX files.
if [ -z $auth_code ]; then
if [ -r .authcode ]; then
auth_code=`head -1 .authcode`
elif [ -r ~/.authcode ]; then
auth_code=`head -1 ~/.authcode`
fi
fi
# -----
# Check the target dir for if set if it is writable
if [[ "x${targetdir}" != "x" ]]; then
if [[ ! -w "${targetdir}" || ! -d "${targetdir}" ]] ; then
echo "ERROR Target Directory does not exist or is not writable: \"$targetdir\""
echo "$usage"
exit 1
fi
fi
# -----
# Check the completed dir for if set if it is writable
if [[ "x${completedir}" != "x" ]]; then
if [[ ! -w "${completedir}" || ! -d "${completedir}" ]] ; then
echo "ERROR Complete Directory does not exist or is not writable: \"$completedir\""
echo "$usage"
exit 1
fi
fi
# -----
# Check whether the loglevel is valid
if [ "$((${loglevel} < 0 || ${loglevel} > 3 ))" = "1" ]; then
echo "ERROR loglevel has to be in the range from 0 to 3!"
echo " 0: Show progress only"
echo " 1: default"
echo " 2: a little more information, timestamps"
echo " 3: debug"
echo "$usage"
exit 1
fi
# -----
# If a compression level is given, check whether the given codec supports compression level specifiers and whether the level is valid.
if [ "${level}" != "-1" ]; then
if [ "${codec}" == "flac" ]; then
if [ "$((${level} < 0 || ${level} > 12 ))" = "1" ]; then
echo "ERROR Flac compression level has to be in the range from 0 to 12!"
echo "$usage"
exit 1
fi
elif [ "${codec}" == "libopus" ]; then
if [ "$((${level} < 0 || ${level} > 10 ))" = "1" ]; then
echo "ERROR Opus compression level has to be in the range from 0 to 10!"
echo "$usage"
exit 1
fi
elif [ "${codec}" == "libmp3lame" ]; then
if [ "$((${level} < 0 || ${level} > 9 ))" = "1" ]; then
echo "ERROR MP3 compression level has to be in the range from 0 to 9!"
echo "$usage"
exit 1
fi
else
echo "ERROR This codec doesnt support compression levels!"
echo "$usage"
exit 1
fi
fi
# -----
# Clean up if someone hits ^c or the script exits for any reason.
trap 'rm -r -f "${working_directory}"' EXIT
# -----
# Set up some basic working files ASAP. Note the trap will clean this up no matter what.
working_directory=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
metadata_file="${working_directory}/metadata.txt"
# -----
# Validate the AAX and extract the metadata associated with the file.
validate_aax() {
local media_file
media_file="$1"
# Test for existence
if [[ ! -r "${media_file}" ]] ; then
log "ERROR File NOT Found: ${media_file}"
return 1
else
if [[ "${VALIDATE}" == "1" ]]; then
log "Test 1 SUCCESS: ${media_file}"
fi
fi
# Clear the errexit value we want to capture the output of the ffprobe below.
set +e errexit
# Take a look at the aax file and see if it is valid. If the source file is aaxc, we give ffprobe additional flags
output="$("$FFPROBE" -loglevel warning ${decrypt_param} -i "${media_file}" 2>&1)"
# If invalid then say something.
if [[ $? != "0" ]] ; then
# No matter what lets bark that something is wrong.
log "ERROR: Invalid File: ${media_file}"
elif [[ "${VALIDATE}" == "1" ]]; then
# If the validate option is present then lets at least state what is valid.
log "Test 2 SUCCESS: ${media_file}"
fi
# This is a big test only performed when the --validate switch is passed.
if [[ "${VALIDATE}" == "1" ]]; then
output="$("$FFMPEG" -hide_banner ${decrypt_param} -i "${media_file}" -vn -f null - 2>&1)"
if [[ $? != "0" ]] ; then
log "ERROR: Invalid File: ${media_file}"
else
log "Test 3 SUCCESS: ${media_file}"
fi
fi
# Dump the output of the ffprobe command.
debug "$output"
# Turn it back on. ffprobe is done.
set -e errexit
}
validate_extra_files() {
local extra_media_file extra_find_command
extra_media_file="$1"
# Bash trick to delete, non greedy, from the end up until the first '-'
extra_title="${extra_media_file%-*}"
# Using this is not ideal, because if the naming scheme is changed then
# this part of the script will break
# AAX file: BookTitle-LC_128_44100_stereo.aax
# Cover file: BookTitle_(1215).jpg
# Chapter file: BookTitle-chapters.json
# Chapter
extra_chapter_file="${extra_title}-chapters.json"
# Cover
extra_dirname="$(dirname "${extra_media_file}")"
extra_find_command='$FIND "${extra_dirname}" -maxdepth 1 -regex ".*/${extra_title##*/}_([0-9]+)\.jpg"'
# We want the output of the find command, we will turn errexit on later
set +e errexit
extra_cover_file="$(eval ${extra_find_command})"
extra_eval_comm="$(eval echo ${extra_find_command})"
set -e errexit
if [[ "${aaxc}" == "1" ]]; then
# bash trick to get file w\o extention (delete from end to the first '.')
extra_voucher="${extra_media_file%.*}.voucher"
if [[ ! -r "${extra_voucher}" ]] ; then
log "ERROR File NOT Found: ${extra_voucher}"
return 1
fi
aaxc_key=$(jq -r '.content_license.license_response.key' "${extra_voucher}")
aaxc_iv=$(jq -r '.content_license.license_response.iv' "${extra_voucher}")
fi
debug_vars "Audible-cli variables" extra_media_file extra_title extra_chapter_file extra_cover_file extra_find_command extra_eval_comm extra_dirname extra_voucher aaxc_key aaxc_iv
# Test for chapter file existence
if [[ ! -r "${extra_chapter_file}" ]] ; then
log "ERROR File NOT Found: ${extra_chapter_file}"
return 1
fi
if [[ "x${extra_cover_file}" == "x" ]] ; then
log "ERROR Cover File NOT Found"
return 1
fi
# 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"
if [[ $(type -P mediainfo) ]]; then
echo "Mediainfo data START" >> "$metadata_file"
# Mediainfo output is structured like ffprobe, so we append it to the metadata file and then parse it with get_metadata_value()
# mediainfo "$media_file" >> "$metadata_file"
# Or we only get the data we are intrested in:
# Description
echo "Track_More :" "$(mediainfo --Inform="General;%Track_More%" "$media_file")" >> "$metadata_file"
# Narrator
echo "nrt :" "$(mediainfo --Inform="General;%nrt%" "$media_file")" >> "$metadata_file"
# Publisher
echo "pub :" "$(mediainfo --Inform="General;%pub%" "$media_file")" >> "$metadata_file"
echo "Mediainfo data END" >> "$metadata_file"
fi
if [[ "${audibleCli}" == "1" ]]; then
# If we use data we got with audible-cli, we delete conflicting chapter infos
$SED -i '/^ Chapter #/d' "${metadata_file}"
# Some magic: we parse the .json generated by audible-cli.
# to get the output structure like the one generated by ffprobe,
# we use some characters (#) as placeholder, add some new lines,
# put a ',' after the start value, we calculate the end of each chapter
# as start+length, and we convert (divide) the time stamps from ms to s.
# Then we delete all ':' 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"
}
# -----
# Reach into the meta data and extract a specific value.
# This is a long pipe of transforms.
# This finds the first occurrence of the key : value pair.
get_metadata_value() {
local key
key="$1"
# Find the key in the meta data file # Extract field value # Remove the following /'s "(Unabridged) <blanks> at start end and multiples.
echo "$($GREP --max-count=1 --only-matching "${key} *: .*" "$metadata_file" | cut -d : -f 2- | $SED -e 's#/##g;s/ (Unabridged)//;s/^[[:blank:]]\+//g;s/[[:blank:]]\+$//g' | $SED 's/[[:blank:]]\+/ /g')"
}
# -----
# specific variant of get_metadata_value bitrate is important for transcoding.
get_bitrate() {
get_metadata_value bitrate | $GREP --only-matching '[0-9]\+'
}
# Save the original value, since in the for loop we overwrite
# $audibleCli in case the file is aaxc. If the file is the
# old aax, reset the variable to be the one passed by the user
originalAudibleCliVar=$audibleCli
# ========================================================================
# Main Transcode Loop
for aax_file
do
# If the file is in aaxc format, set the proper variables
if [[ ${aax_file##*.} == "aaxc" ]]; then
# File is the new .aaxc
aaxc=1
audibleCli=1
else
# File is the old .aax
aaxc=0
# If some previous file in the loop are aaxc, the $audibleCli variable has been overwritten, so we reset it to the original one
audibleCli=$originalAudibleCliVar
fi
debug_vars "Variables set based on file extention" aaxc originalAudibleCliVar audibleCli
# No point going on if no authcode found and the file is aax.
# If we use aaxc as input, we do not need it
# if the string $auth_code is null and the format is not aaxc; quit. We need the authcode
if [ -z $auth_code ] && [ "${aaxc}" = "0" ]; then
echo "ERROR Missing authcode, can't decode $aax_file"
echo "$usage"
exit 1
fi
# Validate the input aax file. Note this happens no matter what.
# It's just that if the validate option is set then we skip to next file.
# If however validate is not set and we proceed with the script any errors will
# case the script to stop.
# If the input file is aaxc, we need to first get the audible_key and audible_iv
# We get them in the function validate_extra_files
if [[ ${audibleCli} == "1" ]] ; then
# If we have additional files (obtained via audible-cli), be sure that they
# exists and they are in the correct location.
validate_extra_files "${aax_file}"
fi
# Set the needed params to decrypt the file. Needed in all command that require ffprobe or ffmpeg
# After validate_extra_files, since the -audible_key and -audible_iv are read in that function
if [[ ${aaxc} == "1" ]] ; then
decrypt_param="-audible_key ${aaxc_key} -audible_iv ${aaxc_iv}"
else
decrypt_param="-activation_bytes ${auth_code}"
fi
validate_aax "${aax_file}"
if [[ ${VALIDATE} == "1" ]] ; then
# Don't bother doing anything else with this file.
continue
fi
# -----
# Make sure everything is a variable. Simplifying Command interpretation
save_metadata "${aax_file}"
genre=$(get_metadata_value genre)
if [ "x${authorOverride}" != "x" ]; then
#Manual Override
artist="${authorOverride}"
album_artist="${authorOverride}"
else
if [ "${keepArtist}" != "-1" ]; then
# Choose artist from the one that are present in the metadata. Comma separated list of names
# remove leading space; 'C. S. Lewis' -> 'C.S. Lewis'
artist="$(get_metadata_value artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')"
album_artist="$(get_metadata_value album_artist | cut -d',' -f"$keepArtist" | $SED -E 's|^ ||g; s|\. +|\.|g; s|((\w+\.)+)|\1 |g')"
else
# The default
artist=$(get_metadata_value artist)
album_artist="$(get_metadata_value album_artist)"
fi
fi
title=$(get_metadata_value title)
title=${title:0:128}
bitrate="$(get_bitrate)k"
album="$(get_metadata_value album)"
album_date="$(get_metadata_value date)"
copyright="$(get_metadata_value copyright)"
series="$(get_metadata_value series)"
series_sequence="$(get_metadata_value series_sequence)"
# Get more tags with mediainfo
if [[ $(type -P mediainfo) ]]; then
narrator="$(get_metadata_value nrt)"
description="$(get_metadata_value Track_More)"
publisher="$(get_metadata_value pub)"
else
narrator=""
description=""
publisher=""
fi
# Define the output_directory
if [ "${customDNS}" == "1" ]; then
currentDirNameScheme="$(eval echo "${dirNameScheme}")"
else
# The Default
currentDirNameScheme="${genre}/${artist}/${title}"
fi
# If we defined a target directory, use it. Otherwise use the location of the AAX file
if [ "x${targetdir}" != "x" ] ; then
output_directory="${targetdir}/${currentDirNameScheme}"
else
output_directory="$(dirname "${aax_file}")/${currentDirNameScheme}"
fi
# Define the output_file
if [ "${customFNS}" == "1" ]; then
currentFileNameScheme="$(eval echo "${fileNameScheme}")"
else
# The Default
currentFileNameScheme="${title}"
fi
output_file="${output_directory}/${currentFileNameScheme}.${extension}"
if [[ "${noclobber}" = "1" ]] && [[ -d "${output_directory}" ]]; then
log "Noclobber enabled but directory '${output_directory}' exists. Skipping to avoid overwriting"
rm -f "${metadata_file}" "${tmp_chapter_file}"
continue
fi
mkdir -p "${output_directory}"
if [ "$((${loglevel} > 0))" = "1" ]; then
# Fancy declaration of which book we are decoding. Including the AUTHCODE.
dashline="----------------------------------------------------"
log "$(printf '\n----Decoding---%s%s--%s--' "${title}" "${dashline:${#title}}" "${auth_code}")"
log "Source: ${aax_file}"
fi
# Big long DEBUG output. Fully describes the settings used for transcoding.
# Note this is a long debug command. It's not critical to operation. It's purely for people debugging
# and coders wanting to extend the script.
debug_vars "Book and Variable values" title auth_code aaxc aaxc_key aaxc_iv mode aax_file container codec bitrate artist album_artist album album_date genre copyright narrator description publisher currentDirNameScheme output_directory currentFileNameScheme output_file metadata_file working_directory
# Display the total length of the audiobook in format hh:mm:ss
# 10#$var force base-10 interpretation. By default it's base-8, so values like 08 or 09 are not octal numbers
total_length="$("$FFPROBE" -v error ${decrypt_param} -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${aax_file}" | cut -d . -f 1)"
hours="$((total_length/3600))"
if [ "$((hours<10))" = "1" ]; then hours="0$hours"; fi
minutes="$((total_length/60-60*10#$hours))"
if [ "$((minutes<10))" = "1" ]; then minutes="0$minutes"; fi
seconds="$((total_length-3600*10#$hours-60*10#$minutes))"
if [ "$((seconds<10))" = "1" ]; then seconds="0$seconds"; fi
log "Total length: $hours:$minutes:$seconds"
# If level != -1 specify a compression level in ffmpeg.
compression_level_param=""
if [ "${level}" != "-1" ]; then
compression_level_param="-compression_level ${level}"
fi
# -----
if [ "${continue}" == "0" ]; then
# This is the main work horse command. This is the primary transcoder.
# This is the primary transcode. All the heavy lifting is here.
debug '"$FFMPEG" -loglevel error -stats ${decrypt_param} -i "${aax_file}" -vn -codec:a "${codec}" -ab ${bitrate} -map_metadata -1 -metadata title="${title}" -metadata artist="${artist}" -metadata album_artist="${album_artist}" -metadata album="${album}" -metadata date="${album_date}" -metadata track="1/1" -metadata genre="${genre}" -metadata copyright="${copyright}" "${output_file}"'
</dev/null "$FFMPEG" -loglevel error \
-stats \
${decrypt_param} \
-i "${aax_file}" \
-vn \
-codec:a "${codec}" \
${compression_level_param} \
-ab ${bitrate} \
-map_metadata -1 \
-metadata title="${title}" \
-metadata artist="${artist}" \
-metadata album_artist="${album_artist}" \
-metadata album="${album}" \
-metadata date="${album_date}" \
-metadata track="1/1" \
-metadata genre="${genre}" \
-metadata copyright="${copyright}" \
-metadata description="${description}" \
-metadata composer="${narrator}" \
-metadata publisher="${publisher}" \
-metadata series="${series}" \
-metadata series_sequence="${series_sequence}" \
-f ${container} \
"${output_file}"
if [ "$((${loglevel} > 0))" == "1" ]; then
log "Created ${output_file}."
fi
# -----
fi
# Grab the cover art if available.
cover_file="${output_directory}/${currentFileNameScheme}.jpg"
if [ "${continue}" == "0" ]; then
if [ "${audibleCli}" == "1" ]; then
# We have a better quality cover file, copy it.
if [ "$((${loglevel} > 1))" == "1" ]; then
log "Copy cover file to ${cover_file}..."
fi
cp "${extra_cover_file}" "${cover_file}"
else
# Audible-cli not used, extract the cover from the aax file
if [ "$((${loglevel} > 1))" == "1" ]; then
log "Extracting cover into ${cover_file}..."
fi
</dev/null "$FFMPEG" -loglevel error -activation_bytes "${auth_code}" -i "${aax_file}" -an -codec:v copy "${cover_file}"
fi
fi
extra_crop_cover=''
cover_width=$(ffprobe -i "${cover_file}" 2>&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)
if [ "${mode}" == "chaptered" ]; then
# Playlist m3u support
playlist_file="${output_directory}/${currentFileNameScheme}.m3u"
if [ "${continue}" == "0" ]; then
if [ "$((${loglevel} > 0))" == "1" ]; then
log "Creating PlayList ${currentFileNameScheme}.m3u"
fi
echo '#EXTM3U' > "${playlist_file}"
fi
# Determine the number of chapters.
chaptercount=$($GREP -Pc "Chapter.*start.*end" $metadata_file)
if [ "$((${loglevel} > 0))" == "1" ]; then
log "Extracting ${chaptercount} chapter files from ${output_file}..."
if [ "${continue}" == "1" ]; then
log "Continuing at chapter ${continueAt}:"
fi
fi
chapternum=1
#start progressbar for loglevel 0 and 1
if [ "$((${loglevel} < 2))" == "1" ]; then
progressbar 0 ${chaptercount}
fi
# We pipe the metadata_file in read.
# Example of the section that we are interested in:
#
# Chapter #0:0: start 0.000000, end 1928.231474
# Metadata:
# title : Chapter 1
#
# Then read the line in these variables:
# first Chapter
# _ #0:0:
# _ start
# chapter_start 0.000000,
# _ end
# chapter_end 1928.231474
while read -r -u9 first _ _ chapter_start _ chapter_end
do
# Do things only if the line starts with 'Chapter'
if [[ "${first}" = "Chapter" ]]; then
# The next line (Metadata:...) gets discarded
read -r -u9 _
# From the line 'title : Chapter 1' we save the third field and those after in chapter
read -r -u9 _ _ chapter
# The formatting of the chapters names and the file names.
# Chapter names are used in a few place.
# Define the chapter_file
if [ "${customCNS}" == "1" ]; then
chapter_title="$(eval echo "${chapterNameScheme}")"
else
# The Default
chapter_title="${title}-$(printf %0${#chaptercount}d $chapternum) ${chapter}"
fi
chapter_file="${output_directory}/${chapter_title}.${extension}"
# Since the .aax file allready got converted we can use
# -acodec copy, which is much faster than a reencodation.
# Since there is an issue when using copy on flac, where
# the duration of the chapters gets shown as if they where
# as long as the whole audiobook.
chapter_codec=""
if test "${extension}" = "flac"; then
chapter_codec="flac "${compression_level_param}""
else
chapter_codec="copy"
fi
#Since there seems to be a bug in some older versions of ffmpeg, which makes, that -ss and -to
#have to be apllied to the output file, this makes, that -ss and -to get applied on the input for
#ffmpeg version 4+ and on the output for all older versions.
split_input=""
split_output=""
if [ "$(($("$FFMPEG" -version | $SED -E 's/[^0-9]*([0-9]).*/\1/g;1q') > 3))" = "1" ]; then
split_input="-ss ${chapter_start%?} -to ${chapter_end}"
else
split_output="-ss ${chapter_start%?} -to ${chapter_end}"
fi
# Big Long chapter debug
debug_vars "Chapter Variables:" cover_file chapter_start chapter_end chapternum chapter chapterNameScheme chapter_title chapter_file
if [ "$((${continueAt} > ${chapternum}))" = "0" ]; then
# Extract chapter by time stamps start and finish of chapter.
# This extracts based on time stamps start and end.
if [ "$((${loglevel} > 1))" == "1" ]; then
log "Splitting chapter ${chapternum}/${chaptercount} start:${chapter_start%?}(s) end:${chapter_end}(s)"
fi
</dev/null "$FFMPEG" -loglevel quiet \
-nostats \
${split_input} \
-i "${output_file}" \
-i "${cover_file}" \
${extra_crop_cover} \
${split_output} \
-map 0:0 \
-map 1:0 \
-acodec ${chapter_codec} \
-metadata:s:v title="Album cover" \
-metadata:s:v comment="Cover (front)" \
-metadata track="${chapternum}" \
-metadata title="${chapter}" \
-metadata:s:a title="${chapter}" \
-metadata:s:a track="${chapternum}" \
-map_chapters -1 \
-f ${container} \
"${chapter_file}"
# -----
if [ "$((${loglevel} < 2))" == "1" ]; then
progressbar ${chapternum} ${chaptercount}
fi
# OK lets get what need for the next chapter in the Playlist m3u file.
# Playlist creation.
duration=$(echo "${chapter_end} - ${chapter_start%?}" | bc)
echo "#EXTINF:${duration%.*},${title} - ${chapter}" >> "${playlist_file}"
echo "${chapter_title}.${extension}" >> "${playlist_file}"
fi
chapternum=$((chapternum + 1 ))
fi
done 9< "$metadata_file"
# Clean up of working directory stuff.
rm "${output_file}"
if [ "$((${loglevel} > 1))" == "1" ]; then
log "Done creating chapters for ${output_directory}."
else
#ending progress bar
echo ""
fi
else
# Perform file tasks on output file.
# ----
# ffmpeg seems to copy only chapter position, not chapter names.
# use already created chapter.txt from save_metadata() in case
# audible-cli data is used else use ffprobe to extract from m4b
if [[ ${container} == "mp4" && $(type -P mp4chaps) ]]; then
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
</dev/null "$FFMPEG" -loglevel quiet \
-nostats \
-i "${output_file}" \
-i "${cover_file}" \
-map 0:a:0 \
-map 1:v:0 \
-acodec copy \
-vcodec copy \
-id3v2_version 3 \
-metadata:s:v title="Album cover" \
-metadata:s:v comment="Cover (front)" \
"${cover_output_file}"
# Replace original output file with version including cover
mv "${cover_output_file}" "${output_file}"
fi
fi
# -----
# Announce that we have completed the transcode
if [ "$((${loglevel} > 0))" == "1" ]; then
log "Complete ${title}"