Added Cancel method to stop download/decrypt and added estimated time remaining event.

This commit is contained in:
Michael Bucari-Tovo 2021-06-28 15:21:59 -06:00
parent b65f9567e0
commit f0eb57a40b
9 changed files with 146 additions and 93 deletions

View File

@ -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<int> 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<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> 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<AaxcDownloadConverter> 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);
}
/// <summary>
/// Calculates the average processing rate based on the last <see cref="MAX_NUM_AVERAGE"/> samples.
/// </summary>
/// <param name="lastProcessedPosition">Position in the audio file last processed</param>
/// <returns>The average processing rate, in book_duration_seconds / second.</returns>
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<StreamPosition> streamPositions = new Queue<StreamPosition>();
/// <summary>
/// Copy all aacx metadata to m4b file, including cover art.
/// </summary>
@ -207,5 +271,10 @@ namespace AaxDecrypter
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters));
return true;
}
public void Cancel()
{
aaxcProcesser.Cancel();
}
}
}

View File

@ -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))

View File

@ -22,10 +22,12 @@ namespace FileLiberator.AaxcDownloadDecrypt
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<TimeSpan> UpdateRemainingTime;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
private AaxcDownloadConverter aaxcDownloader;
public async Task<StatusHandler> 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();
}
}
}

View File

@ -11,7 +11,9 @@ namespace FileLiberator
event EventHandler<string> NarratorsDiscovered;
event EventHandler<byte[]> CoverImageFilepathDiscovered;
event EventHandler<int> UpdateProgress;
event EventHandler<TimeSpan> UpdateRemainingTime;
event EventHandler<string> DecryptCompleted;
void Cancel();
}
}

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>4.4.0.121</Version>
<Version>4.4.0.181</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -1,64 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@ -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
}