From 78f278121b9745f9ea3d72336605d4796b9e9422 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 29 Jun 2021 22:04:54 -0600 Subject: [PATCH] Add book info to DecryptForm from LibraryBook on initialization. --- FileLiberator/DownloadDecryptBook.cs | 205 ++++++++++++++++++ .../BookLiberation/DecryptForm.cs | 4 +- .../ProcessorAutomationController.cs | 59 +++-- 3 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 FileLiberator/DownloadDecryptBook.cs diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs new file mode 100644 index 00000000..8eed437e --- /dev/null +++ b/FileLiberator/DownloadDecryptBook.cs @@ -0,0 +1,205 @@ +using DataLayer; +using Dinah.Core; +using Dinah.Core.ErrorHandling; +using FileManager; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AaxDecrypter; +using AudibleApi; + +namespace FileLiberator +{ + public class DownloadDecryptBook : IDecryptable + { + public event EventHandler Begin; + public event EventHandler DecryptBegin; + public event EventHandler TitleDiscovered; + public event EventHandler AuthorsDiscovered; + public event EventHandler NarratorsDiscovered; + public event EventHandler CoverImageFilepathDiscovered; + public event EventHandler UpdateProgress; + public event EventHandler UpdateRemainingTime; + public event EventHandler DecryptCompleted; + public event EventHandler Completed; + public event EventHandler StatusUpdate; + + private AaxcDownloadConverter aaxcDownloader; + public async Task ProcessAsync(LibraryBook libraryBook) + { + Begin?.Invoke(this, libraryBook); + + try + { + if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)) + return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; + + var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DecryptInProgress, libraryBook); + + // decrypt failed + if (outputAudioFilename is null) + return new StatusHandler { "Decrypt failed" }; + + // moves files and returns dest dir. Do not put inside of if(RetainAaxFiles) + _ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); + + var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId); + if (!finalAudioExists) + return new StatusHandler { "Cannot find final audio file after decryption" }; + + return new StatusHandler(); + } + finally + { + Completed?.Invoke(this, libraryBook); + } + } + + private async Task aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook) + { + DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}"); + + try + { + validate(libraryBook); + + var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale); + + var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); + + var aaxcDecryptDlLic = new DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent); + + var destinationDirectory = Path.GetDirectoryName(destinationDir); + + if (Configuration.Instance.DownloadChapters) + { + var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId); + + var aaxcDecryptChapters = new ChapterInfo(); + + foreach (var chap in contentMetadata?.ChapterInfo?.Chapters) + aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs)); + + aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters); + } + else + { + aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic); + } + + aaxcDownloader.AppName = "Libation"; + + TitleDiscovered?.Invoke(this, aaxcDownloader.Title); + AuthorsDiscovered?.Invoke(this, aaxcDownloader.Author); + NarratorsDiscovered?.Invoke(this, aaxcDownloader.Narrator); + + if (aaxcDownloader.CoverArt is not null) + CoverImageFilepathDiscovered?.Invoke(this, aaxcDownloader.CoverArt); + + // override default which was set in CreateAsync + var proposedOutputFile = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].m4b"); + aaxcDownloader.SetOutputFilename(proposedOutputFile); + aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); + aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining); + + // REAL WORK DONE HERE + var success = await Task.Run(() => aaxcDownloader.Run()); + + // decrypt failed + if (!success) + return null; + + return aaxcDownloader.outputFileName; + } + finally + { + DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}"); + } + } + + private static string moveFilesToBooksDir(Book product, string outputAudioFilename) + { + // create final directory. move each file into it. MOVE AUDIO FILE LAST + // new dir: safetitle_limit50char + " [" + productId + "]" + + var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId); + Directory.CreateDirectory(destinationDir); + + var sortedFiles = getProductFilesSorted(product, outputAudioFilename); + + var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.'); + + // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext + var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId); + + foreach (var f in sortedFiles) + { + var dest + = AudibleFileStorage.Audio.IsFileTypeMatch(f) + ? audioFileName + // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext + : FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt); + + if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") + Cue.UpdateFileName(f, audioFileName); + + File.Move(f.FullName, dest); + } + + return destinationDir; + } + + private static List getProductFilesSorted(Book product, string outputAudioFilename) + { + // files are: temp path\author\[asin].ext + var m4bDir = new FileInfo(outputAudioFilename).Directory; + var files = m4bDir + .EnumerateFiles() + .Where(f => f.Name.ContainsInsensitive(product.AudibleProductId)) + .ToList(); + + // move audio files to the end of the collection so these files are moved last + var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f)); + var sortedFiles = files + .Except(musicFiles) + .Concat(musicFiles) + .ToList(); + + return sortedFiles; + } + + private static void validate(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")); + } + + public bool Validate(LibraryBook libraryBook) + => !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId) + && !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId); + + public void Cancel() + { + aaxcDownloader.Cancel(); + } + } +} diff --git a/LibationWinForms/BookLiberation/DecryptForm.cs b/LibationWinForms/BookLiberation/DecryptForm.cs index 1f76bafd..0c03c76f 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.cs +++ b/LibationWinForms/BookLiberation/DecryptForm.cs @@ -56,8 +56,8 @@ namespace LibationWinForms.BookLiberation private void updateBookInfo() => bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}"); - public void SetCoverImage(byte[] coverBytes) - => pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes)); + public void SetCoverImage(System.Drawing.Image coverImage) + => pictureBox1.UIThread(() => pictureBox1.Image = coverImage); public void UpdateProgress(int percentage) { diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index acc79518..da9ca8a1 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -25,7 +25,7 @@ namespace LibationWinForms.BookLiberation logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text); logMe.LogErrorString += (_, text) => form.WriteLine(text); - logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error"); + logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error"); logMe.LogError += (_, tuple) => { form.WriteLine(tuple.Item2 ?? "Automated backup: error"); @@ -70,7 +70,7 @@ namespace LibationWinForms.BookLiberation { var backupBook = new BackupBook(); - backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook); + backupBook.DecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DecryptBook, l); backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf); // must occur before completedAction. A common use case is: @@ -141,7 +141,7 @@ namespace LibationWinForms.BookLiberation var downloadPdf = getWiredUpDownloadPdf(completedAction); - (AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf); + (AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf); await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync(); } @@ -239,19 +239,30 @@ namespace LibationWinForms.BookLiberation } // subscribed to Begin event because a new form should be created+processed+closed on each iteration - private static void wireUpEvents(IDecryptable decryptBook) + private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook) { #region create form var decryptDialog = new DecryptForm(); #endregion + #region Set initially displayed book properties from library info. + decryptDialog.SetTitle(libraryBook.Book.Title); + decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors)); + decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames)); + decryptDialog.SetCoverImage( + WindowsDesktopUtilities.WinAudibleImageServer.GetImage( + libraryBook.Book.PictureId, + FileManager.PictureSize._80x80 + )); + #endregion + #region define how model actions will affect form behavior void decryptBegin(object _, string __) => decryptDialog.Show(); void titleDiscovered(object _, string title) => decryptDialog.SetTitle(title); void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors); void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators); - void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(coverBytes); + void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes)); void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage); void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining); @@ -407,23 +418,23 @@ Created new 'skip' file { private LibraryBook _libraryBook { get; } - protected override string SkipDialogText => @" + protected override string SkipDialogText => @" An error occurred while trying to process this book. Skip this book permanently? - Click YES to skip this book permanently. - Click NO to skip the book this time only. We'll try again later. ".Trim(); - protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo; - protected override DialogResult CreateSkipFileResult => DialogResult.Yes; + protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo; + protected override DialogResult CreateSkipFileResult => DialogResult.Yes; - public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook) + public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook) : base(logMe, processable, automatedBackupsForm) { _libraryBook = libraryBook; } - protected override async Task RunAsync() + protected override async Task RunAsync() { if (_libraryBook is not null) await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook); @@ -431,7 +442,7 @@ An error occurred while trying to process this book. Skip this book permanently? } class BackupLoop : BackupRunner { - protected override string SkipDialogText => @" + protected override string SkipDialogText => @" An error occurred while trying to process this book - ABORT: stop processing books. @@ -450,20 +461,20 @@ An error occurred while trying to process this book { // support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here foreach (var libraryBook in Processable.GetValidLibraryBooks()) - { - var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook); - if (!keepGoing) - return; + { + var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook); + if (!keepGoing) + return; - if (!AutomatedBackupsForm.KeepGoing) - { - if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked) - LogMe.Info("'Keep going' is unchecked"); - return; - } - } + if (!AutomatedBackupsForm.KeepGoing) + { + if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked) + LogMe.Info("'Keep going' is unchecked"); + return; + } + } - LogMe.Info("Done. All books have been processed"); + LogMe.Info("Done. All books have been processed"); } - } + } }