diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index 760d745a..d75a037a 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -46,9 +46,8 @@ namespace AaxDecrypter private TagLib.Mpeg4.File aaxcTagLib { get; set; } private StepSequence steps { get; } private DownloadLicense downloadLicense { get; set; } - private string metadataPath => Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta"); - public static async Task CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters) + public static async Task CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null) { var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters); await converter.prelimProcessing(); @@ -59,7 +58,6 @@ namespace AaxDecrypter { ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory)); ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic)); - ArgumentValidator.EnsureNotNull(chapters, nameof(chapters)); if (!Directory.Exists(outDirectory)) throw new ArgumentNullException(nameof(outDirectory), "Directory does not exist"); @@ -134,27 +132,35 @@ namespace AaxDecrypter public bool Step2_DownloadAndCombine() { - var ffmetaHeader = $";FFMETADATA1\n"; - - File.WriteAllText(metadataPath, ffmetaHeader + chapters.ToFFMeta()); var aaxcProcesser = new FFMpegAaxcProcesser(DecryptSupportLibraries.ffmpegPath); - aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate; + string metadataPath = null; + + if (chapters != null) + { + //Only write chaopters to the metadata file. All other aaxc metadata will be + //wiped out but is restored in Step 3. + metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta"); + File.WriteAllText(metadataPath, chapters.ToFFMeta(true)); + } + aaxcProcesser.ProcessBook( downloadLicense.DownloadUrl, downloadLicense.UserAgent, downloadLicense.AudibleKey, downloadLicense.AudibleIV, - metadataPath, - outputFileName) + outputFileName, + metadataPath) .GetAwaiter() .GetResult(); + if (chapters != null) + FileExt.SafeDelete(metadataPath); + DecryptProgressUpdate?.Invoke(this, 0); - FileExt.SafeDelete(metadataPath); return aaxcProcesser.Succeeded; } @@ -165,6 +171,10 @@ namespace AaxDecrypter DecryptProgressUpdate?.Invoke(this, (int)progressPercent); } + /// + /// Copy all aacx metadata to m4b file. + /// + /// public bool Step3_RestoreMetadata() { var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average); diff --git a/AaxDecrypter/Chapters.cs b/AaxDecrypter/Chapters.cs index 4b4c2966..07ff0202 100644 --- a/AaxDecrypter/Chapters.cs +++ b/AaxDecrypter/Chapters.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Dinah.Core; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,11 +12,16 @@ namespace AaxDecrypter public int Count => _chapterList.Count; public void AddChapter(Chapter chapter) { + ArgumentValidator.EnsureNotNull(chapter, nameof(chapter)); _chapterList.Add(chapter); } - public string ToFFMeta() + public string ToFFMeta(bool includeFFMetaHeader) { var ffmetaChapters = new StringBuilder(); + + if (includeFFMetaHeader) + ffmetaChapters.AppendLine(";FFMETADATA1\n"); + foreach (var c in Chapters) { ffmetaChapters.AppendLine(c.ToFFMeta()); @@ -30,6 +36,10 @@ namespace AaxDecrypter public long EndOffsetMs { get; } public Chapter(string title, long startOffsetMs, long lengthMs) { + ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title)); + ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1); + ArgumentValidator.EnsureGreaterThan(lengthMs, nameof(lengthMs), 0); + Title = title; StartOffsetMs = startOffsetMs; EndOffsetMs = StartOffsetMs + lengthMs; diff --git a/AaxDecrypter/DownloadLicense.cs b/AaxDecrypter/DownloadLicense.cs index 61ecf571..d75b4192 100644 --- a/AaxDecrypter/DownloadLicense.cs +++ b/AaxDecrypter/DownloadLicense.cs @@ -1,4 +1,5 @@ -using System; +using Dinah.Core; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -15,6 +16,11 @@ namespace AaxDecrypter public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent) { + ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); + ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey)); + ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV)); + ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); + DownloadUrl = downloadUrl; AudibleKey = audibleKey; AudibleIV = audibleIV; diff --git a/AaxDecrypter/FFMpegAaxcProcesser.cs b/AaxDecrypter/FFMpegAaxcProcesser.cs index 950fa3d3..9e937c8c 100644 --- a/AaxDecrypter/FFMpegAaxcProcesser.cs +++ b/AaxDecrypter/FFMpegAaxcProcesser.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,17 +16,20 @@ namespace AaxDecrypter public string FFMpegPath { get; } public bool IsRunning { get; private set; } public bool Succeeded { get; private set; } + public string FFMpegStandardError => ffmpegError.ToString(); + + private StringBuilder ffmpegError = new StringBuilder(); private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + public FFMpegAaxcProcesser(string ffmpegPath) { FFMpegPath = ffmpegPath; } - public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string outputFile) + public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string outputFile, string metadataPath = null) { - //This process gets the aaxc from the url and streams the decrypted - //m4b to the output file. Preserves album art, and ignores metadata. + //m4b to the output file. var StartInfo = new ProcessStartInfo { FileName = FFMpegPath, @@ -34,71 +38,47 @@ namespace AaxDecrypter WindowStyle = ProcessWindowStyle.Hidden, UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(FFMpegPath), - ArgumentList = - { - "-audible_key", - audibleKey, - "-audible_iv", - audibleIV, - "-user_agent", - userAgent, - "-i", - aaxcUrl, - "-c", //audio codec - "copy", //copy stream - "-f", //force output format: adts - "mp4", - outputFile, //pipe output to standard output - "-y" - } + }; - await ProcessBool(StartInfo); - } - - public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string metadataPath, string outputFile) - { - - //This process gets the aaxc from the url and streams the decrypted - //m4b to the output file. Preserves album art, but replaces metadata. - var StartInfo = new ProcessStartInfo + if (metadataPath != null) { - FileName = FFMpegPath, - RedirectStandardError = true, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - WorkingDirectory = Path.GetDirectoryName(FFMpegPath), - ArgumentList = - { - "-ignore_chapters", //prevents ffmpeg from copying chapter info from aaxc to output file - "true", - "-audible_key", - audibleKey, - "-audible_iv", - audibleIV, - "-user_agent", - userAgent, - "-i", - aaxcUrl, - "-f", - "ffmetadata", - "-i", - metadataPath, - "-map_metadata", - "1", - "-c", //audio codec - "copy", //copy stream - "-f", //force output format: adts - "mp4", - outputFile, //pipe output to standard output - "-y" - } - }; - await ProcessBool(StartInfo); + StartInfo.ArgumentList.Add("-ignore_chapters"); //prevents ffmpeg from copying chapter info from aaxc to output file + StartInfo.ArgumentList.Add("true"); + } + + StartInfo.ArgumentList.Add("-audible_key"); + StartInfo.ArgumentList.Add(audibleKey); + StartInfo.ArgumentList.Add("-audible_iv"); + StartInfo.ArgumentList.Add(audibleIV); + StartInfo.ArgumentList.Add("-user_agent"); + StartInfo.ArgumentList.Add(userAgent); + StartInfo.ArgumentList.Add("-i"); + StartInfo.ArgumentList.Add(aaxcUrl); + + if (metadataPath != null) + { + StartInfo.ArgumentList.Add("-f"); + StartInfo.ArgumentList.Add("ffmetadata"); + StartInfo.ArgumentList.Add("-i"); + StartInfo.ArgumentList.Add(metadataPath); + StartInfo.ArgumentList.Add("-map_metadata"); + StartInfo.ArgumentList.Add("1"); + } + + StartInfo.ArgumentList.Add("-c"); //copy all codecs to output + StartInfo.ArgumentList.Add("copy"); + StartInfo.ArgumentList.Add("-f"); //force output format: mp4 + StartInfo.ArgumentList.Add("mp4"); + StartInfo.ArgumentList.Add("-movflags"); //don't add nero format chapter flags + StartInfo.ArgumentList.Add("disable_chpl+faststart"); + StartInfo.ArgumentList.Add(outputFile); + StartInfo.ArgumentList.Add("-y"); //overwrite existing file + + await ProcessBook(StartInfo); } - private async Task ProcessBool(ProcessStartInfo startInfo) + private async Task ProcessBook(ProcessStartInfo startInfo) { var downloader = new Process { @@ -124,6 +104,8 @@ namespace AaxDecrypter if (string.IsNullOrEmpty(e.Data)) return; + ffmpegError.AppendLine(e.Data); + if (processedTimeRegex.IsMatch(e.Data)) { //get timestamp of of last processed audio stream position