From 325ea95ebbb830838a5b685b08c978510e1fe77e Mon Sep 17 00:00:00 2001 From: KrumpetPirate Date: Sun, 13 Dec 2015 16:48:30 -0600 Subject: [PATCH] Version 1.0 --- AAXtoMP3.pl | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ AAXtoMP3.py | 37 ------------------ README.md | 39 ++++++++++++++++++- 3 files changed, 143 insertions(+), 39 deletions(-) create mode 100755 AAXtoMP3.pl delete mode 100755 AAXtoMP3.py diff --git a/AAXtoMP3.pl b/AAXtoMP3.pl new file mode 100755 index 0000000..b048dfb --- /dev/null +++ b/AAXtoMP3.pl @@ -0,0 +1,106 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use v5.22.0; +use autodie; +use Getopt::Long qw/GetOptions/; +Getopt::Long::Configure qw/gnu_getopt/; +use File::Basename qw/fileparse/; +use IO::CaptureOutput qw/capture_exec/; + +say "Starting $0..."; + +my $VERBOSE; +my $AUTHCODE; +my $INPUT_FILE; +my $CHAPTER; +GetOptions( + 'auth|a=s' => \$AUTHCODE, + 'input|i=s' => \$INPUT_FILE, + 'verbose|v' => \$VERBOSE, + 'chapter|c' => \$CHAPTER, +) or die "Usage: $0 -verbose -chapter -auth -input "; +die "Usage: $0 -verbose -chapter -auth -input " + unless ($INPUT_FILE && $AUTHCODE); +die "$INPUT_FILE is not a valid path" unless (-f $INPUT_FILE); + +my ($filename, $dir, $extension) = fileparse($INPUT_FILE, qr/\.[^.]*/); +my $ret = system_execute("ffprobe $INPUT_FILE"); + +my @fflines = split /\n/, $ret; +my $file_type = pull_from_ffprobe("major_brand", @fflines); +my $file_title = pull_from_ffprobe("title", @fflines); +my $file_author = pull_from_ffprobe("artist", @fflines); +my @chapters = grep { m/^\s+Chapter/ } @fflines; +die "Input file ffprobe does not appear to match an AAX file type metadata" + unless $file_type =~ /aax/; + +my $output; +if ($file_title && $file_author) { + $output = "$file_title by $file_author"; +} else { + $output = $filename; +} +my $output_sh_safe = quotemeta $output; + +if (@chapters && $CHAPTER) { + say "Attempting to convert AAX to MP3 by chapter. This will increase the processing time..."; + system_execute("mkdir -p $dir$output_sh_safe"); + my $chapter_index = 1; + foreach my $chapter (@chapters) { + my ($start, $duration) = extract_ts($chapter); + my $output_fn = quotemeta "$dir$output/$output (Chapter $chapter_index).mp3"; + say "Converting Chapter $chapter_index START: $start DURATION: $duration..."; + $ret = system_execute("ffmpeg -activation_bytes $AUTHCODE -i $INPUT_FILE -vn -c:a libmp3lame -ab 128k -ss $start -t $duration $output_fn"); + $chapter_index++; + say $ret if $VERBOSE; + } +} else { + say "Using Audible auth code to copy AAX audio directly to MP3 format..."; + $ret = system_execute("ffmpeg -activation_bytes $AUTHCODE -i $INPUT_FILE -vn -c:a libmp3lame -ab 128k $dir$output_sh_safe.mp3"); + say $ret if $VERBOSE; +} + +say "End of $0..."; + +sub system_execute { + my ($stdout, $stderr, $success, $exit_code) = capture_exec(@_); + say "stdout: $stdout\nstderr: $stderr\nsuccess: $success\nexit code: $exit_code" if ($VERBOSE); + if ($success) { + return $stdout if $stdout; + return $stderr if $stderr; + } else { + my $command = join " ", @_; + die "Command $command was not successful, debug."; + } +} + +sub extract_ts { + my $line = shift; + $line =~ s/.*://; + $line =~ s/^\s*(.*?)\s*$/$1/; + my @parts = split /\,/, $line; + my $start = $parts[0]; + my $end = $parts[1]; + $start =~ s/.* //; + $end =~ s/.* //; + my $duration = $end - $start; + return (convert_seconds($start), convert_seconds($duration)); +} + +sub convert_seconds { + my $sec = shift; + my $time = sprintf "%02d:%02d:%02d.%03d", + (gmtime($sec))[2,1,0, 0 % 1000]; +} + +sub pull_from_ffprobe { + my $desired = shift; + my @fflines = @_; + return unless ($desired && @fflines); + my ($line) = grep { m/^\s+$desired/ } @fflines; + $line =~ s/^\s*(.*?)\s*$/$1/; + $line =~ s/.*://; + $line =~ s/^\s*(.*?)\s*$/$1/; + return $line; +} diff --git a/AAXtoMP3.py b/AAXtoMP3.py deleted file mode 100755 index d3a81c8..0000000 --- a/AAXtoMP3.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/python3 -import sys -import getopt -import shutil - -def usage(): - print('Usage: '+sys.argv[0]+' -a -f ') - -def main(argv): - try: - opts, args = getopt.getopt(sys.argv[1:], 'a:f:h', ['auth=', 'file=', 'help']) - except getopt.GetoptError: - usage() - sys.exit(2) - - for opt, arg in opts: - if opt in ('-h', '--help'): - usage() - sys.exit(2) - elif opt in ('-a', '--auth'): - auth_string = arg - elif opt in ('-f', '--file'): - input_file = arg - else: - usage() - sys.exit(2) - if not auth_string: - usage() - sys.exit(2) - if not input_file: - usage() - sys.exit(2) - print(auth_string) - print(input_file) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/README.md b/README.md index 4e3c2c3..7dcfce0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # AAXtoMP3 -The purpose of this software is to convert AAX files to a more common MP3 format. +The purpose of this software is to convert AAX files to a more common MP3 format +through a basic perl script frontend to FFMPEG. Audible uses this file format to maintain DRM restrictions on their audio books and if you download your book through your library it will be @@ -16,8 +17,42 @@ You will need your four byte authitication code that comes from Audible's servers. This will be used by ffmpeg to perform the initial audio convert. You can obtain this string from a tool like [audible-activator](https://github.com/inAudible-NG/audible-activator). -## Anti-Piracy Notice +## Requirements +* perl version 5.22.0 or later +* ffmpeg version 2.8.3 or later +You will also require the following perl modules: +* autodie +* Getopt::Long +* File::Basename +* IO::CaptureOutput + +I would suggest installing cpanminus either through CPAN or (preferably) through your +distro's repositories. Take note that many perl modules can be packaged in your repos +as well so take a look there before resorting to cpan/cpanminus. + +## Usage +``` +perl AAXtoMP3.pl -a AUTHCODE -i AAXFILE -v -c +``` +* -a: **your** Audible auth code (it won't correctly decode otherwise) (required) +* -i: the input AAX file to be converted (required) +* -v: verbose (optional) +* -c: Convert to chapters based on input file's metadata information + +## Known issues +Currently the chapters mode (activated by the -c flag) does not work correctly. +Parsing the metadata works fine but when the timestamps are converted to +HH:MM:SS.xxx format there is some sort of hickup. It could be that Audible has some +delay in the file to prevent this, but starting from Chapter 2 of the book it will +no longer sync. I don't know if I will ever fix this, so avoid using the -c flag for now +to produce one large MP3 file for the audiobook. + +Also this was tested on Linux with the above requirements. No effort will be made to +port this work to any other operating system, though it may work fine. Want a Windows/ +OSX port? You'll have to fork the work. + +## Anti-Piracy Notice Note that this project does NOT ‘crack’ the DRM. It simplys allows the user to use their own encryption key (fetched from Audible servers) to decrypt the audiobook in the same manner that the official audiobook playing software does.