Refactor AaxDecrypter

This commit is contained in:
Michael Bucari-Tovo 2023-01-24 22:25:19 -07:00
parent 4d6c742ae9
commit 25b37c6266
19 changed files with 363 additions and 932 deletions

View file

@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using AAXClean;
using AAXClean;
using Dinah.Core.Net.Http;
using System;
using System.Threading.Tasks;
namespace AaxDecrypter
{
@ -9,8 +9,23 @@ namespace AaxDecrypter
{
public event EventHandler<AppleTags> RetrievedMetadata;
protected AaxFile AaxFile;
protected Mp4Operation aaxConversion;
protected AaxFile AaxFile { get; private set; }
private Mp4Operation aaxConversion;
protected Mp4Operation AaxConversion
{
get => aaxConversion;
set
{
if (aaxConversion is not null)
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
if (value is not null)
{
aaxConversion = value;
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
}
}
}
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { }
@ -23,9 +38,23 @@ namespace AaxDecrypter
AaxFile.AppleTags.Cover = coverArt;
}
public override async Task CancelAsync()
{
IsCanceled = true;
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
FinalizeDownload();
}
protected override void FinalizeDownload()
{
AaxConversion = null;
base.FinalizeDownload();
}
protected bool Step_GetMetadata()
{
AaxFile = new AaxFile(InputFileStream);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
if (DownloadOptions.StripUnabridged)
{
@ -44,7 +73,6 @@ namespace AaxDecrypter
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate);
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
@ -55,40 +83,15 @@ namespace AaxDecrypter
return !IsCanceled;
}
protected DownloadProgress Step_DownloadAudiobook_Start()
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var zeroProgress = new DownloadProgress
{
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length
};
OnDecryptProgressUpdate(zeroProgress);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return zeroProgress;
}
protected void Step_DownloadAudiobook_End(DownloadProgress zeroProgress)
{
AaxFile.Close();
CloseInputFileStream();
OnDecryptProgressUpdate(zeroProgress);
}
protected void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = AaxFile.Duration;
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
var remainingSecsToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = (e.ProcessPosition / e.TotalDuration);
var progressPercent = e.ProcessPosition / e.TotalDuration;
OnDecryptProgressUpdate(
new DownloadProgress
@ -98,14 +101,5 @@ namespace AaxDecrypter
TotalBytesToReceive = InputFileStream.Length
});
}
public override async Task CancelAsync()
{
IsCanceled = true;
if (aaxConversion != null)
await aaxConversion.CancelAsync();
AaxFile?.Close();
CloseInputFileStream();
}
}
}

View file

@ -1,81 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AAXClean;
using AAXClean;
using AAXClean.Codecs;
using FileManager;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
{
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
private FileStream workingFileStream;
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { }
public override async Task<bool> RunAsync()
: base(outFileName, cacheDirectory, dlOptions)
{
try
{
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
//Step 1
Serilog.Log.Information("Begin Get Aaxc Metadata");
if (await Task.Run(Step_GetMetadata))
Serilog.Log.Information("Completed Get Aaxc Metadata");
else
{
Serilog.Log.Information("Failed to Complete Get Aaxc Metadata");
return false;
}
//Step 2
Serilog.Log.Information("Begin Download Decrypted Audiobook");
if (await Step_DownloadAudiobookAsMultipleFilesPerChapter())
Serilog.Log.Information("Completed Download Decrypted Audiobook");
else
{
Serilog.Log.Information("Failed to Complete Download Decrypted Audiobook");
return false;
}
//Step 3
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 4
Serilog.Log.Information("Begin Cleanup");
if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Cleanup");
else
{
Serilog.Log.Information("Failed to Complete Cleanup");
return false;
}
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return true;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return false;
}
AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split";
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
}
/*
@ -102,10 +45,8 @@ The book will be split into the following files:
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
*/
private async Task<bool> Step_DownloadAudiobookAsMultipleFilesPerChapter()
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
var zeroProgress = Step_DownloadAudiobook_Start();
var chapters = DownloadOptions.ChapterInfo.Chapters;
// Ensure split files are at least minChapterLength in duration.
@ -128,110 +69,81 @@ That naming may not be desirable for everyone, but it's an easy change to instea
}
}
// reset, just in case
multiPartFilePaths.Clear();
try
{
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
aaxConversion = ConvertToMultiMp4a(splitChapters);
else
aaxConversion = ConvertToMultiMp3(splitChapters);
await (AaxConversion = decryptMultiAsync(splitChapters));
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
await aaxConversion;
if (AaxConversion.IsCompletedSuccessfully)
await moveMoovToBeginning(workingFileStream?.Name);
if (aaxConversion.IsCompletedSuccessfully)
moveMoovToBeginning(workingFileStream?.Name);
return aaxConversion.IsCompletedSuccessfully;
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "AAXClean Error");
workingFileStream?.Close();
if (workingFileStream?.Name is not null)
FileUtility.SaferDelete(workingFileStream.Name);
return false;
return AaxConversion.IsCompletedSuccessfully;
}
finally
{
if (aaxConversion is not null)
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
workingFileStream?.Dispose();
FinalizeDownload();
}
}
private Mp4Operation ConvertToMultiMp4a(ChapterInfo splitChapters)
private Mp4Operation decryptMultiAsync(ChapterInfo splitChapters)
{
var chapterCount = 0;
return AaxFile.ConvertToMultiMp4aAsync
return
DownloadOptions.OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMultiMp4aAsync
(
splitChapters,
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.TrimOutputToChapterLength
);
}
private Mp4Operation ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
return AaxFile.ConvertToMultiMp3Async
)
: AaxFile.ConvertToMultiMp3Async
(
splitChapters,
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.LameConfig,
DownloadOptions.TrimOutputToChapterLength
);
}
private void Callback(int currentChapter, ChapterInfo splitChapters, NewMP3SplitCallback newSplitCallback)
=> Callback(currentChapter, splitChapters, newSplitCallback as NewSplitCallback);
private void Callback(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
MultiConvertFileProperties props = new()
void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
OutputFileName = OutputFileName,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback?.Chapter?.Title,
};
MultiConvertFileProperties props = new()
{
OutputFileName = OutputFileName,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback?.Chapter?.Title,
};
moveMoovToBeginning(workingFileStream?.Name);
moveMoovToBeginning(workingFileStream?.Name).GetAwaiter().GetResult();
newSplitCallback.OutputFile = createOutputFileStream(props);
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitleName(props);
newSplitCallback.TrackNumber = currentChapter;
newSplitCallback.TrackCount = splitChapters.Count;
newSplitCallback.OutputFile = createOutputFileStream(props);
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props);
newSplitCallback.TrackNumber = currentChapter;
newSplitCallback.TrackCount = splitChapters.Count;
FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
FileUtility.SaferDelete(fileName);
workingFileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(fileName);
return workingFileStream;
}
}
}
private void moveMoovToBeginning(string filename)
private Mp4Operation moveMoovToBeginning(string filename)
{
if (DownloadOptions.OutputFormat is OutputFormat.M4b
&& DownloadOptions.MoveMoovToBeginning
&& filename is not null
&& File.Exists(filename))
{
Mp4File.RelocateMoovAsync(filename).GetAwaiter().GetResult();
return Mp4File.RelocateMoovAsync(filename);
}
}
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
var extension = Path.GetExtension(fileName);
fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters, extension);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
workingFileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(fileName);
return workingFileStream;
else return Mp4Operation.CompletedOperation;
}
}
}

View file

@ -1,153 +1,67 @@
using System;
using System.IO;
using System.Threading.Tasks;
using AAXClean;
using AAXClean;
using AAXClean.Codecs;
using FileManager;
using Mpeg4Lib.Util;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
{
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { }
public override async Task<bool> RunAsync()
: base(outFileName, cacheDirectory, dlOptions)
{
try
{
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
//Step 1
Serilog.Log.Information("Begin Step 1: Get Aaxc Metadata");
if (await Task.Run(Step_GetMetadata))
Serilog.Log.Information("Completed Step 1: Get Aaxc Metadata");
else
{
Serilog.Log.Information("Failed to Complete Step 1: Get Aaxc Metadata");
return false;
}
//Step 2
Serilog.Log.Information("Begin Step 2: Download Decrypted Audiobook");
if (await Step_DownloadAudiobookAsSingleFile())
Serilog.Log.Information("Completed Step 2: Download Decrypted Audiobook");
else
{
Serilog.Log.Information("Failed to Complete Step 2: Download Decrypted Audiobook");
return false;
}
//Step 3
Serilog.Log.Information("Begin Step 3: Create Cue");
if (await Task.Run(Step_CreateCue))
Serilog.Log.Information("Completed Step 3: Create Cue");
else
{
Serilog.Log.Information("Failed to Complete Step 3: Create Cue");
return false;
}
//Step 4
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 5
Serilog.Log.Information("Begin Step 4: Cleanup");
if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Step 4: Cleanup");
else
{
Serilog.Log.Information("Failed to Complete Step 4: Cleanup");
return false;
}
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return true;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return false;
}
AsyncSteps.Name = $"Download and Convert Aaxc To {DownloadOptions.OutputFormat}";
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
AsyncSteps["Step 4: Create Cue"] = Step_CreateCueAsync;
}
private async Task<bool> Step_DownloadAudiobookAsSingleFile()
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
var zeroProgress = Step_DownloadAudiobook_Start();
FileUtility.SaferDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
using var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(OutputFileName);
try
{
aaxConversion = decryptAsync(outputFile);
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
await aaxConversion;
await (AaxConversion = decryptAsync(outputFile));
outputFile.Close();
if (aaxConversion.IsCompletedSuccessfully
&& DownloadOptions.OutputFormat is OutputFormat.M4b
&& DownloadOptions.MoveMoovToBeginning)
if (AaxConversion.IsCompletedSuccessfully
&& DownloadOptions.MoveMoovToBeginning
&& DownloadOptions.OutputFormat is OutputFormat.M4b)
{
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
aaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
await aaxConversion;
outputFile.Close();
await (AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName));
}
if (aaxConversion.IsCompletedSuccessfully)
base.OnFileCreated(OutputFileName);
return aaxConversion.IsCompletedSuccessfully;
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "AAXClean Error");
FileUtility.SaferDelete(OutputFileName);
return false;
return AaxConversion.IsCompletedSuccessfully;
}
finally
{
outputFile.Close();
if (aaxConversion is not null)
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
FinalizeDownload();
}
}
private Mp4Operation decryptAsync(Stream outputFile)
=> DownloadOptions.OutputFormat == OutputFormat.Mp3 ?
AaxFile.ConvertToMp3Async
=> DownloadOptions.OutputFormat == OutputFormat.Mp3
? AaxFile.ConvertToMp3Async
(
outputFile,
DownloadOptions.LameConfig,
DownloadOptions.ChapterInfo,
DownloadOptions.TrimOutputToChapterLength
)
: DownloadOptions.FixupFile ?
AaxFile.ConvertToMp4aAsync
(
outputFile,
DownloadOptions.ChapterInfo,
DownloadOptions.TrimOutputToChapterLength
)
: AaxFile.ConvertToMp4aAsync(outputFile);
: DownloadOptions.FixupFile
? AaxFile.ConvertToMp4aAsync
(
outputFile,
DownloadOptions.ChapterInfo,
DownloadOptions.TrimOutputToChapterLength
)
: AaxFile.ConvertToMp4aAsync(outputFile);
}
}

View file

@ -1,9 +1,10 @@
using System;
using Dinah.Core;
using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using FileManager;
using System;
using System.IO;
using System.Threading.Tasks;
using Dinah.Core;
using Dinah.Core.Net.Http;
using FileManager;
namespace AaxDecrypter
{
@ -19,19 +20,17 @@ namespace AaxDecrypter
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public event EventHandler<string> FileCreated;
public bool IsCanceled { get; set; }
public string TempFilePath { get; }
public bool IsCanceled { get; protected set; }
protected string OutputFileName { get; private set; }
protected AsyncStepSequence AsyncSteps { get; } = new();
protected string OutputFileName { get; }
protected IDownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName;
private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState { get; }
private readonly NetworkFileStreamPersister nfsPersister;
private readonly DownloadProgress zeroProgress;
private readonly string jsonDownloadState;
private readonly string tempFilePath;
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
{
@ -45,16 +44,39 @@ namespace AaxDecrypter
Directory.CreateDirectory(cacheDirectory);
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
tempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
nfsPersister = OpenNetworkFileStream();
zeroProgress = new DownloadProgress
{
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length
};
OnDecryptProgressUpdate(zeroProgress);
}
public async Task<bool> RunAsync()
{
AsyncSteps[$"Final Step: Cleanup"] = CleanupAsync;
(bool success, var elapsed) = await AsyncSteps.RunAsync();
var speedup = DownloadOptions.RuntimeLength.TotalSeconds / elapsed.TotalSeconds;
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
return success;
}
public abstract Task CancelAsync();
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
public virtual void SetCoverArt(byte[] coverArt)
{
@ -62,8 +84,6 @@ namespace AaxDecrypter
OnRetrievedCoverArt(coverArt);
}
public abstract Task<bool> RunAsync();
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string authors)
@ -79,13 +99,25 @@ namespace AaxDecrypter
protected void OnFileCreated(string path)
=> FileCreated?.Invoke(this, path);
protected void CloseInputFileStream()
protected virtual void FinalizeDownload()
{
nfsPersister?.NetworkFileStream?.Close();
nfsPersister?.Dispose();
OnDecryptProgressUpdate(zeroProgress);
}
protected bool Step_CreateCue()
protected async Task<bool> Step_DownloadClipsBookmarksAsync()
{
if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks)
{
var recordsFile = await DownloadOptions.SaveClipsAndBookmarksAsync(OutputFileName);
if (File.Exists(recordsFile))
OnFileCreated(recordsFile);
}
return !IsCanceled;
}
protected async Task<bool> Step_CreateCueAsync()
{
if (!DownloadOptions.CreateCueSheet) return true;
@ -93,56 +125,41 @@ namespace AaxDecrypter
try
{
var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters, ".cue");
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
await File.WriteAllTextAsync(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCue)}. FAILED");
Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCueAsync)} Failed");
}
return !IsCanceled;
}
protected bool Step_Cleanup()
private async Task<bool> CleanupAsync()
{
bool success = !IsCanceled;
if (success)
if (IsCanceled) return false;
FileUtility.SaferDelete(jsonDownloadState);
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
!string.IsNullOrEmpty(DownloadOptions.AudibleIV) &&
DownloadOptions.RetainEncryptedFile)
{
FileUtility.SaferDelete(jsonDownloadState);
string aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
FileUtility.SaferMove(tempFilePath, aaxPath);
if (DownloadOptions.AudibleKey is not null &&
DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
FileUtility.SaferMove(TempFilePath, aaxPath);
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
File.WriteAllText(keyPath, $"Key={DownloadOptions.AudibleKey}\r\nIV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);
}
else
FileUtility.SaferDelete(TempFilePath);
OnFileCreated(aaxPath);
OnFileCreated(keyPath);
}
else
FileUtility.SaferDelete(tempFilePath);
return success;
}
protected async Task<bool> Step_DownloadClipsBookmarks()
{
if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks)
{
var recordsFile = await DownloadOptions.SaveClipsAndBookmarks(OutputFileName);
if (File.Exists(recordsFile))
OnFileCreated(recordsFile);
}
return !IsCanceled;
return true;
}
private NetworkFileStreamPersister OpenNetworkFileStream()
@ -151,31 +168,31 @@ namespace AaxDecrypter
try
{
if (!File.Exists(jsonDownloadState))
return nfsp = NewNetworkFilePersister();
return nfsp = newNetworkFilePersister();
nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// If More than ~1 hour has elapsed since getting the download url, it will expire.
// The new url will be to the same file.
// The download url expires after 1 hour.
// The new url points to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
return nfsp;
}
catch
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(TempFilePath);
return nfsp = NewNetworkFilePersister();
FileUtility.SaferDelete(tempFilePath);
return nfsp = newNetworkFilePersister();
}
finally
{
if (nfsp?.NetworkFileStream is not null)
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
}
}
private NetworkFileStreamPersister NewNetworkFilePersister()
{
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
NetworkFileStreamPersister newNetworkFilePersister()
{
var networkFileStream = new NetworkFileStream(tempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}
}
}

View file

@ -1,62 +1,60 @@
using System;
using AAXClean;
using Dinah.Core;
using System.IO;
using System.Text;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public static class Cue
{
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
public static class Cue
{
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var startOffset = chapters.StartOffset;
var startOffset = chapters.StartOffset;
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
trackCount++;
var trackCount = 1;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}");
}
stringBuilder.AppendLine($"TRACK {trackCount++} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds * 75d / 1000):D2}");
}
return stringBuilder.ToString();
}
return stringBuilder.ToString();
}
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(" "))
continue;
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(' '))
continue;
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
File.WriteAllLines(cueFilePath, cueContents);
}
File.WriteAllLines(cueFilePath, cueContents);
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
}

View file

@ -5,13 +5,13 @@ using System.Threading.Tasks;
namespace AaxDecrypter
{
public interface IDownloadOptions
{
{
event EventHandler<long> DownloadSpeedChanged;
FileManager.ReplacementCharacters ReplacementCharacters { get; }
string DownloadUrl { get; }
string UserAgent { get; }
string AudibleKey { get; }
string AudibleIV { get; }
TimeSpan RuntimeLength { get; }
OutputFormat OutputFormat { get; }
bool TrimOutputToChapterLength { get; }
bool RetainEncryptedFile { get; }
@ -26,7 +26,7 @@ namespace AaxDecrypter
bool MatchSourceBitrate { get; }
bool MoveMoovToBeginning { get; }
string GetMultipartFileName(MultiConvertFileProperties props);
string GetMultipartTitleName(MultiConvertFileProperties props);
Task<string> SaveClipsAndBookmarks(string fileName);
}
string GetMultipartTitle(MultiConvertFileProperties props);
Task<string> SaveClipsAndBookmarksAsync(string fileName);
}
}

View file

@ -1,7 +1,5 @@
using AAXClean;
using NAudio.Lame;
using System;
using System.Linq;
namespace AaxDecrypter
{

View file

@ -1,15 +1,13 @@
using System;
using System.IO;
using FileManager;
namespace AaxDecrypter
{
public class MultiConvertFileProperties
{
public string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string Title { get; set; }
public DateTime FileDate { get; } = DateTime.Now;
}
public class MultiConvertFileProperties
{
public string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string Title { get; set; }
public DateTime FileDate { get; } = DateTime.Now;
}
}

View file

@ -3,7 +3,6 @@ using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
@ -83,16 +82,13 @@ namespace AaxDecrypter
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string> requestHeaders = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
SaveFilePath = ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
Uri = ArgumentValidator.EnsureNotNull(uri, nameof(uri));
WritePosition = ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new();
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
@ -109,8 +105,8 @@ namespace AaxDecrypter
#region Downloader
/// <summary> Update the <see cref="JsonFilePersister"/>. </summary>
private void Update()
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
private void OnUpdate()
{
RequestHeaders["Range"] = $"bytes={WritePosition}-";
try
@ -167,7 +163,7 @@ namespace AaxDecrypter
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
return Task.Run(async () => await DownloadFile(networkStream), _cancellationSource.Token);
return Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
}
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
@ -184,7 +180,7 @@ namespace AaxDecrypter
int bytesRead;
do
{
bytesRead = await networkStream.ReadAsync(buff, 0, DOWNLOAD_BUFF_SZ, _cancellationSource.Token);
bytesRead = await networkStream.ReadAsync(buff, _cancellationSource.Token);
await _writeFile.WriteAsync(buff, 0, bytesRead, _cancellationSource.Token);
downloadPosition += bytesRead;
@ -193,7 +189,7 @@ namespace AaxDecrypter
{
await _writeFile.FlushAsync(_cancellationSource.Token);
WritePosition = downloadPosition;
Update();
OnUpdate();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
_downloadedPiece.Set();
}
@ -233,19 +229,12 @@ namespace AaxDecrypter
networkStream.Close();
_writeFile.Close();
_downloadedPiece.Set();
Update();
OnUpdate();
}
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
=> new JsonSerializerSettings();
#endregion
#region Download Stream Reader
[JsonIgnore]
@ -289,7 +278,7 @@ namespace AaxDecrypter
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return IsCancelled ? 0: _readFile.Read(buffer, offset, count);
return IsCancelled ? 0 : _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
@ -306,7 +295,7 @@ namespace AaxDecrypter
}
/// <summary>Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns. </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
/// <param name="requiredPosition">The minimum required flushed data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (WritePosition < requiredPosition
@ -317,20 +306,31 @@ namespace AaxDecrypter
}
}
public override void Close()
{
_cancellationSource.Cancel();
_backgroundDownloadTask?.Wait();
private bool disposed = false;
_readFile.Close();
_writeFile.Close();
Update();
/*
* https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.dispose?view=net-7.0
*
* In derived classes, do not override the Close() method, instead, put all of the
* Stream cleanup logic in the Dispose(Boolean) method.
*/
protected override void Dispose(bool disposing)
{
if (disposing && !disposed)
{
_cancellationSource.Cancel();
_backgroundDownloadTask?.GetAwaiter().GetResult();
_downloadedPiece?.Dispose();
_cancellationSource?.Dispose();
_readFile.Dispose();
_writeFile.Dispose();
OnUpdate();
}
disposed = true;
base.Dispose(disposing);
}
#endregion
~NetworkFileStream()
{
_downloadedPiece?.Close();
}
}
}

View file

@ -1,11 +1,9 @@
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace AaxDecrypter
{
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
{
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
{
/// <summary>Alias for Target </summary>
public NetworkFileStream NetworkFileStream => Target;
@ -17,7 +15,10 @@ namespace AaxDecrypter
public NetworkFileStreamPersister(string path, string jsonPath = null)
: base(path, jsonPath) { }
protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings();
}
protected override void Dispose(bool disposing)
{
NetworkFileStream?.Dispose();
base.Dispose(disposing);
}
}
}

View file

@ -1,97 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core.Net.Http;
using Dinah.Core.Net.Http;
using FileManager;
using System;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
{
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic) { }
public override async Task<bool> RunAsync()
: base(outFileName, cacheDirectory, dlLic)
{
try
{
Serilog.Log.Information("Begin downloading unencrypted audiobook.");
//Step 1
Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata");
if (await Task.Run(Step_GetMetadata))
Serilog.Log.Information("Completed Step 1: Get Mp3 Metadata");
else
{
Serilog.Log.Information("Failed to Complete Step 1: Get Mp3 Metadata");
return false;
}
//Step 2
Serilog.Log.Information("Begin Step 2: Download Audiobook");
if (await Task.Run(Step_DownloadAudiobookAsSingleFile))
Serilog.Log.Information("Completed Step 2: Download Audiobook");
else
{
Serilog.Log.Information("Failed to Complete Step 2: Download Audiobook");
return false;
}
//Step 3
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 4
Serilog.Log.Information("Begin Step 3: Cleanup");
if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Step 3: Cleanup");
else
{
Serilog.Log.Information("Failed to Complete Step 3: Cleanup");
return false;
}
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return true;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
return false;
}
AsyncSteps.Name = "Download Unencrypted Audiobook";
AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
AsyncSteps["Step 2: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
}
public override Task CancelAsync()
{
IsCanceled = true;
CloseInputFileStream();
FinalizeDownload();
return Task.CompletedTask;
}
protected bool Step_GetMetadata()
{
OnRetrievedCoverArt(null);
return !IsCanceled;
}
private bool Step_DownloadAudiobookAsSingleFile()
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
DateTime startTime = DateTime.Now;
// MUST put InputFileStream.Length first, because it starts background downloader.
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
{
var rate = InputFileStream.WritePosition / (DateTime.Now - startTime).TotalSeconds;
@ -109,14 +47,17 @@ namespace AaxDecrypter
BytesReceived = (long)(InputFileStream.Length * progressPercent),
TotalBytesToReceive = InputFileStream.Length
});
Thread.Sleep(200);
await Task.Delay(200);
}
CloseInputFileStream();
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters);
SetOutputFileName(realOutputFileName);
OnFileCreated(realOutputFileName);
FinalizeDownload();
if (!IsCanceled)
{
FileUtility.SaferMove(InputFileStream.SaveFilePath, OutputFileName);
OnFileCreated(OutputFileName);
}
return !IsCanceled;
}