From f0eb57a40bd5c398e528be49cf8545ab21759484 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Mon, 28 Jun 2021 15:21:59 -0600 Subject: [PATCH] Added Cancel method to stop download/decrypt and added estimated time remaining event. --- AaxDecrypter/AaxcDownloadConverter.cs | 77 ++++++++++++++++++- AaxDecrypter/FFMpegAaxcProcesser.cs | 12 ++- .../DownloadDecryptBook.cs | 31 ++++---- FileLiberator/IDecryptable.cs | 2 + LibationLauncher/LibationLauncher.csproj | 2 +- .../BookLiberation/DecryptForm.Designer.cs | 37 ++++++--- .../BookLiberation/DecryptForm.cs | 13 +++- .../BookLiberation/DecryptForm.resx | 62 +-------------- .../ProcessorAutomationController.cs | 3 + 9 files changed, 146 insertions(+), 93 deletions(-) diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index 89d624c8..8eda64ea 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -3,13 +3,14 @@ using Dinah.Core.Diagnostics; using Dinah.Core.IO; using Dinah.Core.StepRunner; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace AaxDecrypter { - public interface ISimpleAaxToM4bConverter2 + public interface ISimpleAaxToM4bConverter { event EventHandler DecryptProgressUpdate; bool Run(); @@ -23,8 +24,9 @@ namespace AaxDecrypter string Narrator { get; } byte[] CoverArt { get; } } - public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter2 + public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter { + void Cancel(); bool Step1_CreateDir(); bool Step2_DownloadAndCombine(); bool Step3_RestoreMetadata(); @@ -34,6 +36,7 @@ namespace AaxDecrypter public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter { public event EventHandler DecryptProgressUpdate; + public event EventHandler DecryptTimeRemaining; public string AppName { get; set; } = nameof(AaxcDownloadConverter); public string outDir { get; private set; } public string outputFileName { get; private set; } @@ -46,7 +49,7 @@ namespace AaxDecrypter private TagLib.Mpeg4.File aaxcTagLib { get; set; } private StepSequence steps { get; } private DownloadLicense downloadLicense { get; set; } - + private FFMpegAaxcProcesser aaxcProcesser; public static async Task CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null) { var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters); @@ -132,7 +135,7 @@ namespace AaxDecrypter public bool Step2_DownloadAndCombine() { - var aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense); + aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense); aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate; bool userSuppliedChapters = chapters != null; @@ -166,11 +169,72 @@ namespace AaxDecrypter private void AaxcProcesser_ProgressUpdate(object sender, TimeSpan e) { + double averageRate = getAverageProcessRate(e); + + double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e).TotalSeconds; + + double estTimeRemaining = remainingSecsToProcess / averageRate; + + if (double.IsNormal(estTimeRemaining)) + DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining)); + + double progressPercent = 100 * e.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds; DecryptProgressUpdate?.Invoke(this, (int)progressPercent); } + /// + /// Calculates the average processing rate based on the last samples. + /// + /// Position in the audio file last processed + /// The average processing rate, in book_duration_seconds / second. + private double getAverageProcessRate(TimeSpan lastProcessedPosition) + { + streamPositions.Enqueue(new StreamPosition + { + ProcessPosition = lastProcessedPosition, + EventTime = DateTime.Now, + }); + + if (streamPositions.Count < 2) + return double.PositiveInfinity; + + //Calculate the harmonic mean of the last AVERAGE_NUM progress updates + //Units are Book_Duration_Seconds / second + + var lastPos = streamPositions.Count > MAX_NUM_AVERAGE ? streamPositions.Dequeue() : null; + + double harmonicDenominator = 0; + int harmonicNumerator = 0; + + foreach (var pos in streamPositions) + { + if (lastPos is null) + { + lastPos = pos; + continue; + } + double dP = (pos.ProcessPosition - lastPos.ProcessPosition).TotalSeconds; + double dT = (pos.EventTime - lastPos.EventTime).TotalSeconds; + + harmonicDenominator += dT / dP; + harmonicNumerator++; + lastPos = pos; + } + + double harmonicMean = harmonicNumerator / harmonicDenominator; + return harmonicMean; + } + private const int MAX_NUM_AVERAGE = 15; + private class StreamPosition + { + public TimeSpan ProcessPosition { get; set; } + public DateTime EventTime { get; set; } + } + + private Queue streamPositions = new Queue(); + /// /// Copy all aacx metadata to m4b file, including cover art. /// @@ -207,5 +271,10 @@ namespace AaxDecrypter File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters)); return true; } + + public void Cancel() + { + aaxcProcesser.Cancel(); + } } } diff --git a/AaxDecrypter/FFMpegAaxcProcesser.cs b/AaxDecrypter/FFMpegAaxcProcesser.cs index 10d38b1d..89734dcf 100644 --- a/AaxDecrypter/FFMpegAaxcProcesser.cs +++ b/AaxDecrypter/FFMpegAaxcProcesser.cs @@ -25,6 +25,8 @@ namespace AaxDecrypter private StringBuilder remuxerError = new StringBuilder(); private StringBuilder downloaderError = new StringBuilder(); private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private Process downloader; + private Process remuxer; public FFMpegAaxcProcesser( DownloadLicense downloadLicense) { @@ -36,14 +38,14 @@ namespace AaxDecrypter { //This process gets the aaxc from the url and streams the decrypted //aac stream to standard output - var downloader = new Process + 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. - var remuxer = new Process + remuxer = new Process { StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath) }; @@ -90,7 +92,11 @@ namespace AaxDecrypter IsRunning = false; Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0; } - + public void Cancel() + { + if (IsRunning && !remuxer.HasExited) + remuxer.Kill(); + } private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e) { if (string.IsNullOrEmpty(e.Data)) diff --git a/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs index 09e9cc65..4ae53569 100644 --- a/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs +++ b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs @@ -22,10 +22,12 @@ namespace FileLiberator.AaxcDownloadDecrypt 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); @@ -74,7 +76,6 @@ namespace FileLiberator.AaxcDownloadDecrypt var destinationDirectory = Path.GetDirectoryName(destinationDir); - AaxcDownloadConverter newDownloader; if (Configuration.Instance.DownloadChapters) { var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId); @@ -84,33 +85,34 @@ namespace FileLiberator.AaxcDownloadDecrypt foreach (var chap in contentMetadata?.ChapterInfo?.Chapters) aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs)); - newDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters); + aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters); } else { - newDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic); + aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic); } - newDownloader.AppName = "Libation"; + aaxcDownloader.AppName = "Libation"; - TitleDiscovered?.Invoke(this, newDownloader.Title); - AuthorsDiscovered?.Invoke(this, newDownloader.Author); - NarratorsDiscovered?.Invoke(this, newDownloader.Narrator); - CoverImageFilepathDiscovered?.Invoke(this, newDownloader.CoverArt); + TitleDiscovered?.Invoke(this, aaxcDownloader.Title); + AuthorsDiscovered?.Invoke(this, aaxcDownloader.Author); + NarratorsDiscovered?.Invoke(this, aaxcDownloader.Narrator); + CoverImageFilepathDiscovered?.Invoke(this, aaxcDownloader.CoverArt); // override default which was set in CreateAsync var proposedOutputFile = Path.Combine(destinationDir, $"{libraryBook.Book.Title} [{libraryBook.Book.AudibleProductId}].m4b"); - newDownloader.SetOutputFilename(proposedOutputFile); - newDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); + 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(() => newDownloader.Run()); + var success = await Task.Run(() => aaxcDownloader.Run()); // decrypt failed if (!success) return null; - return newDownloader.outputFileName; + return aaxcDownloader.outputFileName; } finally { @@ -195,6 +197,9 @@ namespace FileLiberator.AaxcDownloadDecrypt => !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId) && !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId); - + public void Cancel() + { + aaxcDownloader.Cancel(); + } } } diff --git a/FileLiberator/IDecryptable.cs b/FileLiberator/IDecryptable.cs index 14a75812..1c90ccfc 100644 --- a/FileLiberator/IDecryptable.cs +++ b/FileLiberator/IDecryptable.cs @@ -11,7 +11,9 @@ namespace FileLiberator event EventHandler NarratorsDiscovered; event EventHandler CoverImageFilepathDiscovered; event EventHandler UpdateProgress; + event EventHandler UpdateRemainingTime; event EventHandler DecryptCompleted; + void Cancel(); } } diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index d9e0a784..3d4757c8 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 4.4.0.121 + 4.4.0.181 diff --git a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs b/LibationWinForms/BookLiberation/DecryptForm.Designer.cs index 5f9a6a8c..19572211 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs +++ b/LibationWinForms/BookLiberation/DecryptForm.Designer.cs @@ -32,14 +32,16 @@ this.bookInfoLbl = new System.Windows.Forms.Label(); this.progressBar1 = new System.Windows.Forms.ProgressBar(); this.rtbLog = new System.Windows.Forms.RichTextBox(); + this.remainingTimeLbl = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); this.SuspendLayout(); // // pictureBox1 // - this.pictureBox1.Location = new System.Drawing.Point(12, 12); + this.pictureBox1.Location = new System.Drawing.Point(14, 14); + this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.pictureBox1.Name = "pictureBox1"; - this.pictureBox1.Size = new System.Drawing.Size(100, 100); + this.pictureBox1.Size = new System.Drawing.Size(117, 115); this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; this.pictureBox1.TabIndex = 0; this.pictureBox1.TabStop = false; @@ -47,9 +49,10 @@ // bookInfoLbl // this.bookInfoLbl.AutoSize = true; - this.bookInfoLbl.Location = new System.Drawing.Point(118, 12); + this.bookInfoLbl.Location = new System.Drawing.Point(138, 14); + this.bookInfoLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.bookInfoLbl.Name = "bookInfoLbl"; - this.bookInfoLbl.Size = new System.Drawing.Size(100, 13); + this.bookInfoLbl.Size = new System.Drawing.Size(121, 15); this.bookInfoLbl.TabIndex = 0; this.bookInfoLbl.Text = "[multi-line book info]"; // @@ -57,9 +60,10 @@ // this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.progressBar1.Location = new System.Drawing.Point(12, 526); + this.progressBar1.Location = new System.Drawing.Point(14, 607); + this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.progressBar1.Name = "progressBar1"; - this.progressBar1.Size = new System.Drawing.Size(582, 23); + this.progressBar1.Size = new System.Drawing.Size(611, 27); this.progressBar1.TabIndex = 2; // // rtbLog @@ -67,21 +71,33 @@ this.rtbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.rtbLog.Location = new System.Drawing.Point(12, 118); + this.rtbLog.Location = new System.Drawing.Point(14, 136); + this.rtbLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.rtbLog.Name = "rtbLog"; - this.rtbLog.Size = new System.Drawing.Size(582, 402); + this.rtbLog.Size = new System.Drawing.Size(678, 463); this.rtbLog.TabIndex = 1; this.rtbLog.Text = ""; // + // remainingTimeLbl + // + this.remainingTimeLbl.Location = new System.Drawing.Point(632, 607); + this.remainingTimeLbl.Name = "remainingTimeLbl"; + this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31); + this.remainingTimeLbl.TabIndex = 3; + this.remainingTimeLbl.Text = "ETA:\r\n"; + this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight; + // // DecryptForm // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(606, 561); + this.ClientSize = new System.Drawing.Size(707, 647); + this.Controls.Add(this.remainingTimeLbl); this.Controls.Add(this.rtbLog); this.Controls.Add(this.progressBar1); this.Controls.Add(this.bookInfoLbl); this.Controls.Add(this.pictureBox1); + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.Name = "DecryptForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "DecryptForm"; @@ -99,5 +115,6 @@ private System.Windows.Forms.Label bookInfoLbl; private System.Windows.Forms.ProgressBar progressBar1; private System.Windows.Forms.RichTextBox rtbLog; + private System.Windows.Forms.Label remainingTimeLbl; } } \ No newline at end of file diff --git a/LibationWinForms/BookLiberation/DecryptForm.cs b/LibationWinForms/BookLiberation/DecryptForm.cs index 275a7f07..1f76bafd 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.cs +++ b/LibationWinForms/BookLiberation/DecryptForm.cs @@ -59,6 +59,17 @@ namespace LibationWinForms.BookLiberation public void SetCoverImage(byte[] coverBytes) => pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes)); - public void UpdateProgress(int percentage) => progressBar1.UIThread(() => progressBar1.Value = percentage); + public void UpdateProgress(int percentage) + { + if (percentage == 0) + remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec"); + + progressBar1.UIThread(() => progressBar1.Value = percentage); + } + + public void UpdateRemainingTime(TimeSpan remaining) + { + remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{(int)remaining.TotalSeconds} sec"); + } } } diff --git a/LibationWinForms/BookLiberation/DecryptForm.resx b/LibationWinForms/BookLiberation/DecryptForm.resx index 1af7de15..f298a7be 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.resx +++ b/LibationWinForms/BookLiberation/DecryptForm.resx @@ -1,64 +1,4 @@ - - - + diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index 2b350866..53029435 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -264,6 +264,7 @@ namespace LibationWinForms.BookLiberation void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators); void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(coverBytes); void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage); + void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining); void decryptCompleted(object _, string __) => decryptDialog.Close(); #endregion @@ -276,6 +277,7 @@ namespace LibationWinForms.BookLiberation decryptBook.NarratorsDiscovered += narratorsDiscovered; decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered; decryptBook.UpdateProgress += updateProgress; + decryptBook.UpdateRemainingTime += updateRemainingTime; decryptBook.DecryptCompleted += decryptCompleted; #endregion @@ -293,6 +295,7 @@ namespace LibationWinForms.BookLiberation decryptBook.UpdateProgress -= updateProgress; decryptBook.DecryptCompleted -= decryptCompleted; + decryptBook.Cancel(); }; #endregion }