244 lines
9.1 KiB
C#
244 lines
9.1 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AaxDecrypter
|
|
{
|
|
class AaxcProcessUpdate
|
|
{
|
|
public AaxcProcessUpdate(TimeSpan position, double speed)
|
|
{
|
|
ProcessPosition = position;
|
|
ProcessSpeed = speed;
|
|
EventTime = DateTime.Now;
|
|
}
|
|
public TimeSpan ProcessPosition { get; }
|
|
public double ProcessSpeed { get; }
|
|
public DateTime EventTime { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Download audible aaxc, decrypt, and remux with chapters.
|
|
/// </summary>
|
|
class FFMpegAaxcProcesser
|
|
{
|
|
public event EventHandler<AaxcProcessUpdate> ProgressUpdate;
|
|
public string FFMpegPath { get; }
|
|
public DownloadLicense DownloadLicense { get; }
|
|
public bool IsRunning { get; private set; }
|
|
public bool Succeeded { get; private set; }
|
|
public string FFMpegRemuxerStandardError => remuxerError.ToString();
|
|
public string FFMpegDownloaderStandardError => downloaderError.ToString();
|
|
|
|
|
|
private StringBuilder remuxerError { get; } = new StringBuilder();
|
|
private StringBuilder downloaderError { get; } = new StringBuilder();
|
|
private static Regex processedTimeRegex { get; } = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}.*speed=\\s{0,1}([0-9]*[.]?[0-9]+)(?:e\\+([0-9]+)){0,1}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
private Process downloader;
|
|
private Process remuxer;
|
|
private bool isCanceled = false;
|
|
|
|
public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
|
|
{
|
|
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
|
|
DownloadLicense = downloadLicense;
|
|
}
|
|
|
|
public async Task ProcessBook(string outputFile, string ffmetaChaptersPath = null)
|
|
{
|
|
//This process gets the aaxc from the url and streams the decrypted
|
|
//aac stream to standard output
|
|
downloader = new Process
|
|
{
|
|
StartInfo = getDownloaderStartInfo()
|
|
};
|
|
|
|
//This process retreves an aac stream from standard input and muxes
|
|
// it into an m4b along with the cover art and metadata.
|
|
remuxer = new Process
|
|
{
|
|
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
|
|
};
|
|
|
|
IsRunning = true;
|
|
|
|
downloader.ErrorDataReceived += Downloader_ErrorDataReceived;
|
|
downloader.Start();
|
|
downloader.BeginErrorReadLine();
|
|
|
|
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
|
|
remuxer.Start();
|
|
remuxer.BeginErrorReadLine();
|
|
|
|
//Thic check needs to be placed after remuxer has started
|
|
if (isCanceled) return;
|
|
|
|
var pipedOutput = downloader.StandardOutput.BaseStream;
|
|
var pipedInput = remuxer.StandardInput.BaseStream;
|
|
|
|
|
|
//All the work done here. Copy download standard output into
|
|
//remuxer standard input
|
|
await Task.Run(() =>
|
|
{
|
|
int lastRead = 0;
|
|
byte[] buffer = new byte[32 * 1024];
|
|
|
|
do
|
|
{
|
|
lastRead = pipedOutput.Read(buffer, 0, buffer.Length);
|
|
pipedInput.Write(buffer, 0, lastRead);
|
|
} while (lastRead > 0 && !remuxer.HasExited);
|
|
});
|
|
|
|
//Closing input stream terminates remuxer
|
|
pipedInput.Close();
|
|
|
|
//If the remuxer exited due to failure, downloader will still have
|
|
//data in the pipe. Force kill downloader to continue.
|
|
if (remuxer.HasExited && !downloader.HasExited)
|
|
downloader.Kill();
|
|
|
|
remuxer.WaitForExit();
|
|
downloader.WaitForExit();
|
|
|
|
IsRunning = false;
|
|
Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0;
|
|
}
|
|
public void Cancel()
|
|
{
|
|
isCanceled = true;
|
|
|
|
if (IsRunning && !remuxer.HasExited)
|
|
remuxer.Kill();
|
|
if (IsRunning && !downloader.HasExited)
|
|
downloader.Kill();
|
|
}
|
|
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(e.Data))
|
|
return;
|
|
|
|
downloaderError.AppendLine(e.Data);
|
|
}
|
|
|
|
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(e.Data))
|
|
return;
|
|
|
|
remuxerError.AppendLine(e.Data);
|
|
|
|
if (processedTimeRegex.IsMatch(e.Data))
|
|
{
|
|
//get timestamp of of last processed audio stream position
|
|
//and processing speed
|
|
var match = processedTimeRegex.Match(e.Data);
|
|
|
|
int hours = int.Parse(match.Groups[1].Value);
|
|
int minutes = int.Parse(match.Groups[2].Value);
|
|
int seconds = int.Parse(match.Groups[3].Value);
|
|
|
|
var position = new TimeSpan(hours, minutes, seconds);
|
|
|
|
double speed = double.Parse(match.Groups[4].Value);
|
|
int exp = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
|
|
speed *= Math.Pow(10, exp);
|
|
|
|
ProgressUpdate?.Invoke(this, new AaxcProcessUpdate(position, speed));
|
|
}
|
|
|
|
if (e.Data.Contains("aac bitstream error"))
|
|
{
|
|
//This happens if input is corrupt (should never happen) or if caller
|
|
//supplied wrong key/iv
|
|
var process = sender as Process;
|
|
process.Kill();
|
|
}
|
|
}
|
|
|
|
private ProcessStartInfo getDownloaderStartInfo() =>
|
|
new ProcessStartInfo
|
|
{
|
|
FileName = FFMpegPath,
|
|
RedirectStandardError = true,
|
|
RedirectStandardOutput = true,
|
|
CreateNoWindow = true,
|
|
WindowStyle = ProcessWindowStyle.Hidden,
|
|
UseShellExecute = false,
|
|
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
|
ArgumentList ={
|
|
"-nostdin",
|
|
"-audible_key",
|
|
DownloadLicense.AudibleKey,
|
|
"-audible_iv",
|
|
DownloadLicense.AudibleIV,
|
|
"-user_agent",
|
|
DownloadLicense.UserAgent, //user-agent is requied for CDN to serve the file
|
|
"-i",
|
|
DownloadLicense.DownloadUrl,
|
|
"-c:a", //audio codec
|
|
"copy", //copy stream
|
|
"-f", //force output format: adts
|
|
"adts",
|
|
"pipe:" //pipe output to stdout
|
|
}
|
|
};
|
|
|
|
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
|
|
{
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = FFMpegPath,
|
|
RedirectStandardError = true,
|
|
RedirectStandardInput = true,
|
|
CreateNoWindow = true,
|
|
WindowStyle = ProcessWindowStyle.Hidden,
|
|
UseShellExecute = false,
|
|
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
|
};
|
|
|
|
startInfo.ArgumentList.Add("-thread_queue_size");
|
|
startInfo.ArgumentList.Add("1024");
|
|
startInfo.ArgumentList.Add("-f"); //force input format: aac
|
|
startInfo.ArgumentList.Add("aac");
|
|
startInfo.ArgumentList.Add("-i"); //read input from stdin
|
|
startInfo.ArgumentList.Add("pipe:");
|
|
|
|
if (ffmetaChaptersPath is null)
|
|
{
|
|
//copy metadata from aaxc file.
|
|
startInfo.ArgumentList.Add("-user_agent");
|
|
startInfo.ArgumentList.Add(DownloadLicense.UserAgent);
|
|
startInfo.ArgumentList.Add("-i");
|
|
startInfo.ArgumentList.Add(DownloadLicense.DownloadUrl);
|
|
}
|
|
else
|
|
{
|
|
//copy metadata from supplied metadata file
|
|
startInfo.ArgumentList.Add("-f");
|
|
startInfo.ArgumentList.Add("ffmetadata");
|
|
startInfo.ArgumentList.Add("-i");
|
|
startInfo.ArgumentList.Add(ffmetaChaptersPath);
|
|
}
|
|
|
|
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
|
|
startInfo.ArgumentList.Add("0");
|
|
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file 1 (either metadata file or aaxc file)
|
|
startInfo.ArgumentList.Add("1");
|
|
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
|
|
startInfo.ArgumentList.Add("copy");
|
|
startInfo.ArgumentList.Add("-f"); //force output format: mp4
|
|
startInfo.ArgumentList.Add("mp4");
|
|
startInfo.ArgumentList.Add("-movflags");
|
|
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
|
|
startInfo.ArgumentList.Add(outputFile);
|
|
startInfo.ArgumentList.Add("-y"); //overwrite existing
|
|
|
|
return startInfo;
|
|
}
|
|
}
|
|
} |