From 159f5cbd0040d1b411e307d0542a5f654e5e299c Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Mon, 13 Jun 2022 21:55:06 -0600 Subject: [PATCH] Add lame options to ConvertToMp3 --- Source/AaxDecrypter/AudiobookDownloadBase.cs | 10 +- Source/FileLiberator/AudioDecodable.cs | 28 +- Source/FileLiberator/ConvertToMp3.cs | 131 +++--- Source/FileLiberator/DownloadDecryptBook.cs | 467 +++++++++---------- 4 files changed, 320 insertions(+), 316 deletions(-) diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 21cb77d1..3ce6c432 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -21,7 +21,8 @@ namespace AaxDecrypter public event EventHandler FileCreated; public bool IsCanceled { get; set; } - + public string TempFilePath { get; } + protected string OutputFileName { get; private set; } protected DownloadOptions DownloadOptions { get; } protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream; @@ -33,7 +34,6 @@ namespace AaxDecrypter private NetworkFileStreamPersister nfsPersister; private string jsonDownloadState { get; } - public string TempFilePath { get; } protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic) { @@ -53,7 +53,7 @@ namespace AaxDecrypter // delete file after validation is complete FileUtility.SaferDelete(OutputFileName); - } + } public abstract void Cancel(); @@ -65,7 +65,7 @@ namespace AaxDecrypter public bool Run() { - var (IsSuccess, Elapsed) = Steps.Run(); + var (IsSuccess, _) = Steps.Run(); if (!IsSuccess) Serilog.Log.Logger.Error("Conversion failed"); @@ -79,10 +79,8 @@ namespace AaxDecrypter => RetrievedAuthors?.Invoke(this, authors); protected void OnRetrievedNarrators(string narrators) => RetrievedNarrators?.Invoke(this, narrators); - protected void OnRetrievedCoverArt(byte[] coverArt) => RetrievedCoverArt?.Invoke(this, coverArt); - protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress) => DecryptProgressUpdate?.Invoke(this, downloadProgress); protected void OnDecryptTimeRemaining(TimeSpan timeRemaining) diff --git a/Source/FileLiberator/AudioDecodable.cs b/Source/FileLiberator/AudioDecodable.cs index 9b441efb..f54e4b25 100644 --- a/Source/FileLiberator/AudioDecodable.cs +++ b/Source/FileLiberator/AudioDecodable.cs @@ -1,4 +1,6 @@ -using System; +using LibationFileManager; +using NAudio.Lame; +using System; namespace FileLiberator { @@ -12,6 +14,30 @@ namespace FileLiberator public event EventHandler CoverImageDiscovered; public abstract void Cancel(); + protected LameConfig GetLameOptions(Configuration config) + { + LameConfig lameConfig = new(); + lameConfig.Mode = MPEGMode.Mono; + + if (config.LameTargetBitrate) + { + if (config.LameConstantBitrate) + lameConfig.BitRate = config.LameBitrate; + else + { + lameConfig.ABRRateKbps = config.LameBitrate; + lameConfig.VBR = VBRMode.ABR; + lameConfig.WriteVBRTag = true; + } + } + else + { + lameConfig.VBR = VBRMode.Default; + lameConfig.VBRQuality = config.LameVBRQuality; + lameConfig.WriteVBRTag = true; + } + return lameConfig; + } protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title); protected void OnTitleDiscovered(object _, string title) { diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index 65b12bbc..6cf99936 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using AAXClean; using AAXClean.Codecs; using DataLayer; -using Dinah.Core; using Dinah.Core.ErrorHandling; using Dinah.Core.Net.Http; using FileManager; @@ -12,85 +11,85 @@ using LibationFileManager; namespace FileLiberator { - public class ConvertToMp3 : AudioDecodable - { - public override string Name => "Convert to Mp3"; - private Mp4File m4bBook; + public class ConvertToMp3 : AudioDecodable + { + public override string Name => "Convert to Mp3"; + private Mp4File m4bBook; - private long fileSize; - private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3"); + private long fileSize; + private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3"); - public override void Cancel() - { - m4bBook?.Cancel(); - } + public override void Cancel() + { + m4bBook?.Cancel(); + } - public static bool ValidateMp3(LibraryBook libraryBook) + public static bool ValidateMp3(LibraryBook libraryBook) { - var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId); - return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path)); - } + var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId); + return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path)); + } - public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook); + public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook); - public override async Task ProcessAsync(LibraryBook libraryBook) - { - OnBegin(libraryBook); + public override async Task ProcessAsync(LibraryBook libraryBook) + { + OnBegin(libraryBook); - try - { - var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId); - m4bBook = new Mp4File(m4bPath, FileAccess.Read); - m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; + try + { + var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId); + m4bBook = new Mp4File(m4bPath, FileAccess.Read); + m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; - fileSize = m4bBook.InputStream.Length; + fileSize = m4bBook.InputStream.Length; - OnTitleDiscovered(m4bBook.AppleTags.Title); - OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor); - OnNarratorsDiscovered(m4bBook.AppleTags.Narrator); - OnCoverImageDiscovered(m4bBook.AppleTags.Cover); + OnTitleDiscovered(m4bBook.AppleTags.Title); + OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor); + OnNarratorsDiscovered(m4bBook.AppleTags.Narrator); + OnCoverImageDiscovered(m4bBook.AppleTags.Cover); - using var mp3File = File.OpenWrite(Path.GetTempFileName()); + using var mp3File = File.OpenWrite(Path.GetTempFileName()); + var lameConfig = GetLameOptions(Configuration.Instance); + var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File, lameConfig)); + m4bBook.InputStream.Close(); + mp3File.Close(); - var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File)); - m4bBook.InputStream.Close(); - mp3File.Close(); + var proposedMp3Path = Mp3FileName(m4bPath); + var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); + OnFileCreated(libraryBook, realMp3Path); - var proposedMp3Path = Mp3FileName(m4bPath); - var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); - OnFileCreated(libraryBook, realMp3Path); + if (result == ConversionResult.Failed) + return new StatusHandler { "Conversion failed" }; + else if (result == ConversionResult.Cancelled) + return new StatusHandler { "Cancelled" }; + else + return new StatusHandler(); + } + finally + { + OnCompleted(libraryBook); + } + } - if (result == ConversionResult.Failed) - return new StatusHandler { "Conversion failed" }; - else if (result == ConversionResult.Cancelled) - return new StatusHandler { "Cancelled" }; - else - return new StatusHandler(); - } - finally - { - OnCompleted(libraryBook); - } - } + private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) + { + var duration = m4bBook.Duration; + var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds; + var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; - private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) - { - var duration = m4bBook.Duration; - var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds; - var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; + if (double.IsNormal(estTimeRemaining)) + OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); - if (double.IsNormal(estTimeRemaining)) - OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); + double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; - double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds; - - OnStreamingProgressChanged( - new DownloadProgress - { - ProgressPercentage = progressPercent, - BytesReceived = (long)(fileSize * progressPercent), - TotalBytesToReceive = fileSize - }); - } - } + OnStreamingProgressChanged( + new DownloadProgress + { + ProgressPercentage = progressPercent, + BytesReceived = (long)(fileSize * progressPercent), + TotalBytesToReceive = fileSize + }); + } + } } diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 49159f17..5a12657e 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -14,241 +14,222 @@ using LibationFileManager; namespace FileLiberator { - public class DownloadDecryptBook : AudioDecodable - { - public override string Name => "Download & Decrypt"; - private AudiobookDownloadBase abDownloader; + public class DownloadDecryptBook : AudioDecodable + { + public override string Name => "Download & Decrypt"; + private AudiobookDownloadBase abDownloader; - public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); + public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); - public override void Cancel() => abDownloader?.Cancel(); + public override void Cancel() => abDownloader?.Cancel(); - public override async Task ProcessAsync(LibraryBook libraryBook) - { - var entries = new List(); - // these only work so minimally b/c CacheEntry is a record. - // in case of parallel decrypts, only capture the ones for this book id. - // if user somehow starts multiple decrypts of the same book in parallel: on their own head be it - void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e) - { - if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) - entries.Add(e); - } - void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e) - { - if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) - entries.Remove(e); - } - - OnBegin(libraryBook); - - try - { - if (libraryBook.Book.Audio_Exists()) - return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; - - bool success = false; - try - { - FilePathCache.Inserted += FilePathCache_Inserted; - FilePathCache.Removed += FilePathCache_Removed; - - success = await downloadAudiobookAsync(libraryBook); - } - finally - { - FilePathCache.Inserted -= FilePathCache_Inserted; - FilePathCache.Removed -= FilePathCache_Removed; - } - - // decrypt failed - if (!success) - { - foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC)) - FileUtility.SaferDelete(tmpFile.Path); - - return abDownloader?.IsCanceled == true ? - new StatusHandler { "Cancelled" } : - new StatusHandler { "Decrypt failed" }; - } - - // moves new files from temp dir to final dest. - // This could take a few seconds if moving hundreds of files. - var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); - - // decrypt failed - if (!movedAudioFile) - return new StatusHandler { "Cannot find final audio file after decryption" }; - - if (Configuration.Instance.DownloadCoverArt) - DownloadCoverArt(libraryBook); - - libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated); - - return new StatusHandler(); - } - finally - { - OnCompleted(libraryBook); - } - } - - private async Task downloadAudiobookAsync(LibraryBook libraryBook) - { - var config = Configuration.Instance; - - downloadValidation(libraryBook); - - var api = await libraryBook.GetApiAsync(); - var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); - var audiobookDlLic = BuildDownloadOptions(config, contentLic); - - var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.OutputFormat.ToString().ToLower()); - var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; - - if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm) - abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic); - else - { - AaxcDownloadConvertBase converter - = config.SplitFilesByChapter ? new AaxcDownloadMultiConverter( - outFileName, cacheDir, audiobookDlLic, - AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)) - : new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic); - - if (config.AllowLibationFixup) - converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames()); - - abDownloader = converter; - } - - abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged; - abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining; - abDownloader.RetrievedTitle += OnTitleDiscovered; - abDownloader.RetrievedAuthors += OnAuthorsDiscovered; - abDownloader.RetrievedNarrators += OnNarratorsDiscovered; - abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; - abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); - - // REAL WORK DONE HERE - var success = await Task.Run(abDownloader.Run); - - return success; - } - - private static DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic) - { - //I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3. - //I also assume that if DrmType != Adrm, the file will be an mp3. - //These assumptions may be wrong, and only time and bug reports will tell. - - bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm; - - var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ? - OutputFormat.Mp3 : OutputFormat.M4b; - - var audiobookDlLic = new DownloadOptions - ( - contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, - Resources.USER_AGENT - ) - { - AudibleKey = contentLic?.Voucher?.Key, - AudibleIV = contentLic?.Voucher?.Iv, - OutputFormat = outputFormat, - TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio, - RetainEncryptedFile = config.RetainAaxFile && encrypted, - StripUnabridged = config.AllowLibationFixup && config.StripUnabridged, - Downsample = config.AllowLibationFixup && config.LameDownsampleMono, - MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate, - CreateCueSheet = config.CreateCueSheet - }; - - if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3) - { - long startMs = audiobookDlLic.TrimOutputToChapterLength ? - contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0; - - audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs)); - - for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++) - { - var chapter = contentLic.ContentMetadata.ChapterInfo.Chapters[i]; - long chapLenMs = chapter.LengthMs; - - if (i == 0) - chapLenMs -= startMs; - - if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1) - chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs; - - audiobookDlLic.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs)); - } - } - - audiobookDlLic.LameConfig = new(); - audiobookDlLic.LameConfig.Mode = NAudio.Lame.MPEGMode.Mono; - - if (config.LameTargetBitrate) + public override async Task ProcessAsync(LibraryBook libraryBook) + { + var entries = new List(); + // these only work so minimally b/c CacheEntry is a record. + // in case of parallel decrypts, only capture the ones for this book id. + // if user somehow starts multiple decrypts of the same book in parallel: on their own head be it + void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e) { - if (config.LameConstantBitrate) - audiobookDlLic.LameConfig.BitRate = config.LameBitrate; - else - { - audiobookDlLic.LameConfig.ABRRateKbps = config.LameBitrate; - audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.ABR; - audiobookDlLic.LameConfig.WriteVBRTag = true; - } + if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) + entries.Add(e); } + void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e) + { + if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) + entries.Remove(e); + } + + OnBegin(libraryBook); + + try + { + if (libraryBook.Book.Audio_Exists()) + return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; + + bool success = false; + try + { + FilePathCache.Inserted += FilePathCache_Inserted; + FilePathCache.Removed += FilePathCache_Removed; + + success = await downloadAudiobookAsync(libraryBook); + } + finally + { + FilePathCache.Inserted -= FilePathCache_Inserted; + FilePathCache.Removed -= FilePathCache_Removed; + } + + // decrypt failed + if (!success) + { + foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC)) + FileUtility.SaferDelete(tmpFile.Path); + + return abDownloader?.IsCanceled == true ? + new StatusHandler { "Cancelled" } : + new StatusHandler { "Decrypt failed" }; + } + + // moves new files from temp dir to final dest. + // This could take a few seconds if moving hundreds of files. + var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); + + // decrypt failed + if (!movedAudioFile) + return new StatusHandler { "Cannot find final audio file after decryption" }; + + if (Configuration.Instance.DownloadCoverArt) + DownloadCoverArt(libraryBook); + + libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated); + + return new StatusHandler(); + } + finally + { + OnCompleted(libraryBook); + } + } + + private async Task downloadAudiobookAsync(LibraryBook libraryBook) + { + var config = Configuration.Instance; + + downloadValidation(libraryBook); + + var api = await libraryBook.GetApiAsync(); + var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); + var audiobookDlLic = BuildDownloadOptions(config, contentLic); + + var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.OutputFormat.ToString().ToLower()); + var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; + + if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm) + abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic); else { - audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.Default; - audiobookDlLic.LameConfig.VBRQuality = config.LameVBRQuality; - audiobookDlLic.LameConfig.WriteVBRTag = true; - } + AaxcDownloadConvertBase converter + = config.SplitFilesByChapter ? + new AaxcDownloadMultiConverter( + outFileName, cacheDir, audiobookDlLic, + AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)) : + new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic); - return audiobookDlLic; - } + if (config.AllowLibationFixup) + converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames()); - private static void downloadValidation(LibraryBook libraryBook) - { - string errorString(string field) - => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; + abDownloader = converter; + } - string errorTitle() - { - var title - = (libraryBook.Book.Title.Length > 53) - ? $"{libraryBook.Book.Title.Truncate(50)}..." - : libraryBook.Book.Title; - var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; - return errorBookTitle; - }; + abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged; + abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining; + abDownloader.RetrievedTitle += OnTitleDiscovered; + abDownloader.RetrievedAuthors += OnAuthorsDiscovered; + abDownloader.RetrievedNarrators += OnNarratorsDiscovered; + abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; + abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); - if (string.IsNullOrWhiteSpace(libraryBook.Account)) - throw new Exception(errorString("Account")); + // REAL WORK DONE HERE + var success = await Task.Run(abDownloader.Run); - if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) - throw new Exception(errorString("Locale")); - } + return success; + } - private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e) - { - if (e is not null) - OnCoverImageDiscovered(e); - else if (Configuration.Instance.AllowLibationFixup) - abDownloader.SetCoverArt(OnRequestCoverArt()); - } + private DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic) + { + //I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3. + //I also assume that if DrmType != Adrm, the file will be an mp3. + //These assumptions may be wrong, and only time and bug reports will tell. - /// Move new files to 'Books' directory - /// True if audiobook file(s) were successfully created and can be located on disk. Else false. - private static bool moveFilesToBooksDir(LibraryBook libraryBook, List entries) - { - // create final directory. move each file into it - var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook); - Directory.CreateDirectory(destinationDir); + bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm; - FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio); + var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ? + OutputFormat.Mp3 : OutputFormat.M4b; + + var dlOptions = new DownloadOptions + ( + contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, + Resources.USER_AGENT + ) + { + AudibleKey = contentLic?.Voucher?.Key, + AudibleIV = contentLic?.Voucher?.Iv, + OutputFormat = outputFormat, + TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio, + RetainEncryptedFile = config.RetainAaxFile && encrypted, + StripUnabridged = config.AllowLibationFixup && config.StripUnabridged, + Downsample = config.AllowLibationFixup && config.LameDownsampleMono, + MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate, + CreateCueSheet = config.CreateCueSheet, + LameConfig = GetLameOptions(config) + }; + + if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3) + { + long startMs = dlOptions.TrimOutputToChapterLength ? + contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0; + + dlOptions.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs)); + + for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++) + { + var chapter = contentLic.ContentMetadata.ChapterInfo.Chapters[i]; + long chapLenMs = chapter.LengthMs; + + if (i == 0) + chapLenMs -= startMs; + + if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1) + chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs; + + dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs)); + } + } + + return dlOptions; + } + + private static void downloadValidation(LibraryBook libraryBook) + { + string errorString(string field) + => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; + + string errorTitle() + { + var title + = (libraryBook.Book.Title.Length > 53) + ? $"{libraryBook.Book.Title.Truncate(50)}..." + : libraryBook.Book.Title; + var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]"; + return errorBookTitle; + }; + + if (string.IsNullOrWhiteSpace(libraryBook.Account)) + throw new Exception(errorString("Account")); + + if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale)) + throw new Exception(errorString("Locale")); + } + + private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e) + { + if (e is not null) + OnCoverImageDiscovered(e); + else if (Configuration.Instance.AllowLibationFixup) + abDownloader.SetCoverArt(OnRequestCoverArt()); + } + + /// Move new files to 'Books' directory + /// True if audiobook file(s) were successfully created and can be located on disk. Else false. + private static bool moveFilesToBooksDir(LibraryBook libraryBook, List entries) + { + // create final directory. move each file into it + var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook); + Directory.CreateDirectory(destinationDir); + + FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio); if (getFirstAudio() == default) return false; @@ -273,33 +254,33 @@ namespace FileLiberator return true; } - private void DownloadCoverArt(LibraryBook libraryBook) + private void DownloadCoverArt(LibraryBook libraryBook) { - var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook); - var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg"); - coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath)); + var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook); + var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg"); + coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath)); - try - { - if (File.Exists(coverPath)) - FileUtility.SaferDelete(coverPath); + try + { + if (File.Exists(coverPath)) + FileUtility.SaferDelete(coverPath); - (string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ? - (libraryBook.Book.PictureId, PictureSize.Native) : - (libraryBook.Book.PictureLarge, PictureSize.Native); + (string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ? + (libraryBook.Book.PictureId, PictureSize.Native) : + (libraryBook.Book.PictureLarge, PictureSize.Native); - var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size)); - - if (picBytes.Length > 0) - File.WriteAllBytes(coverPath, picBytes); - } - catch (Exception ex) - { - //Failure to download cover art should not be - //considered a failure to download the book - Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product."); - } - } + var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size)); + + if (picBytes.Length > 0) + File.WriteAllBytes(coverPath, picBytes); + } + catch (Exception ex) + { + //Failure to download cover art should not be + //considered a failure to download the book + Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product."); + } + } } }