Merge pull request #462 from Mbucari/master

Refactor AaxDecrypter
This commit is contained in:
rmcrackan 2023-01-25 07:06:37 -05:00 committed by GitHub
commit b7e71f5812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 323 additions and 895 deletions

View File

@ -1,7 +1,7 @@
using System; using AAXClean;
using System.Threading.Tasks;
using AAXClean;
using Dinah.Core.Net.Http; using Dinah.Core.Net.Http;
using System;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
@ -9,8 +9,23 @@ namespace AaxDecrypter
{ {
public event EventHandler<AppleTags> RetrievedMetadata; public event EventHandler<AppleTags> RetrievedMetadata;
protected AaxFile AaxFile; protected AaxFile AaxFile { get; private set; }
protected Mp4Operation aaxConversion; 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) protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { } : base(outFileName, cacheDirectory, dlOptions) { }
@ -23,9 +38,23 @@ namespace AaxDecrypter
AaxFile.AppleTags.Cover = coverArt; 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() protected bool Step_GetMetadata()
{ {
AaxFile = new AaxFile(InputFileStream); AaxFile = new AaxFile(InputFileStream);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
if (DownloadOptions.StripUnabridged) if (DownloadOptions.StripUnabridged)
{ {
@ -44,7 +73,6 @@ namespace AaxDecrypter
DownloadOptions.Downsample, DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate); DownloadOptions.MatchSourceBitrate);
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged); OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]"); OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]"); OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
@ -55,40 +83,15 @@ namespace AaxDecrypter
return !IsCanceled; return !IsCanceled;
} }
protected DownloadProgress Step_DownloadAudiobook_Start() private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{ {
var zeroProgress = new DownloadProgress var remainingSecsToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
{
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 estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining)) if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = (e.ProcessPosition / e.TotalDuration); var progressPercent = e.ProcessPosition / e.TotalDuration;
OnDecryptProgressUpdate( OnDecryptProgressUpdate(
new DownloadProgress new DownloadProgress
@ -98,14 +101,5 @@ namespace AaxDecrypter
TotalBytesToReceive = InputFileStream.Length 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 AAXClean;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AAXClean;
using AAXClean.Codecs; using AAXClean.Codecs;
using FileManager; using FileManager;
using System;
using System.IO;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
{ {
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3); private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
private FileStream workingFileStream; private FileStream workingFileStream;
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { } : base(outFileName, cacheDirectory, dlOptions)
public override async Task<bool> RunAsync()
{ {
try AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split";
{ AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
//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;
}
} }
/* /*
@ -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. 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; var chapters = DownloadOptions.ChapterInfo.Chapters;
// Ensure split files are at least minChapterLength in duration. // Ensure split files are at least minChapterLength in duration.
@ -128,69 +69,42 @@ That naming may not be desirable for everyone, but it's an easy change to instea
} }
} }
// reset, just in case
multiPartFilePaths.Clear();
try try
{ {
if (DownloadOptions.OutputFormat == OutputFormat.M4b) await (AaxConversion = decryptMultiAsync(splitChapters));
aaxConversion = ConvertToMultiMp4a(splitChapters);
else
aaxConversion = ConvertToMultiMp3(splitChapters);
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; if (AaxConversion.IsCompletedSuccessfully)
await aaxConversion; await moveMoovToBeginning(workingFileStream?.Name);
if (aaxConversion.IsCompletedSuccessfully) return 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;
} }
finally finally
{ {
if (aaxConversion is not null) workingFileStream?.Dispose();
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; FinalizeDownload();
Step_DownloadAudiobook_End(zeroProgress);
} }
} }
private Mp4Operation ConvertToMultiMp4a(ChapterInfo splitChapters) private Mp4Operation decryptMultiAsync(ChapterInfo splitChapters)
{ {
var chapterCount = 0; var chapterCount = 0;
return AaxFile.ConvertToMultiMp4aAsync return
DownloadOptions.OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMultiMp4aAsync
( (
splitChapters, splitChapters,
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.TrimOutputToChapterLength DownloadOptions.TrimOutputToChapterLength
); )
} : AaxFile.ConvertToMultiMp3Async
private Mp4Operation ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
return AaxFile.ConvertToMultiMp3Async
( (
splitChapters, splitChapters,
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.LameConfig, DownloadOptions.LameConfig,
DownloadOptions.TrimOutputToChapterLength DownloadOptions.TrimOutputToChapterLength
); );
}
void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
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() MultiConvertFileProperties props = new()
{ {
@ -200,38 +114,34 @@ That naming may not be desirable for everyone, but it's an easy change to instea
Title = newSplitCallback?.Chapter?.Title, Title = newSplitCallback?.Chapter?.Title,
}; };
moveMoovToBeginning(workingFileStream?.Name); moveMoovToBeginning(workingFileStream?.Name).GetAwaiter().GetResult();
newSplitCallback.OutputFile = createOutputFileStream(props); newSplitCallback.OutputFile = workingFileStream = createOutputFileStream(props);
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitleName(props); newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props);
newSplitCallback.TrackNumber = currentChapter; newSplitCallback.TrackNumber = currentChapter;
newSplitCallback.TrackCount = splitChapters.Count; newSplitCallback.TrackCount = splitChapters.Count;
OnFileCreated(workingFileStream.Name);
} }
private void moveMoovToBeginning(string filename) FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
FileUtility.SaferDelete(fileName);
return File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
}
}
private Mp4Operation moveMoovToBeginning(string filename)
{ {
if (DownloadOptions.OutputFormat is OutputFormat.M4b if (DownloadOptions.OutputFormat is OutputFormat.M4b
&& DownloadOptions.MoveMoovToBeginning && DownloadOptions.MoveMoovToBeginning
&& filename is not null && filename is not null
&& File.Exists(filename)) && File.Exists(filename))
{ {
Mp4File.RelocateMoovAsync(filename).GetAwaiter().GetResult(); return Mp4File.RelocateMoovAsync(filename);
} }
} else return Mp4Operation.CompletedOperation;
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;
} }
} }
} }

View File

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

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

View File

@ -1,8 +1,7 @@
using System; using AAXClean;
using Dinah.Core;
using System.IO; using System.IO;
using System.Text; using System.Text;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter namespace AaxDecrypter
{ {
@ -16,15 +15,14 @@ namespace AaxDecrypter
var startOffset = chapters.StartOffset; var startOffset = chapters.StartOffset;
var trackCount = 0; var trackCount = 1;
foreach (var c in chapters.Chapters) foreach (var c in chapters.Chapters)
{ {
var startTime = c.StartOffset - startOffset; var startTime = c.StartOffset - startOffset;
trackCount++;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO"); stringBuilder.AppendLine($"TRACK {trackCount++} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\""); stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}"); stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds * 75d / 1000):D2}");
} }
return stringBuilder.ToString(); return stringBuilder.ToString();
@ -46,7 +44,7 @@ namespace AaxDecrypter
for (var i = 0; i < cueContents.Length; i++) for (var i = 0; i < cueContents.Length; i++)
{ {
var line = cueContents[i]; var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(" ")) if (!line.Trim().StartsWith("FILE") || !line.Contains(' '))
continue; continue;
var fileTypeBegins = line.LastIndexOf(" ") + 1; var fileTypeBegins = line.LastIndexOf(" ") + 1;

View File

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

View File

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

View File

@ -1,6 +1,4 @@
using System; using System;
using System.IO;
using FileManager;
namespace AaxDecrypter namespace AaxDecrypter
{ {

View File

@ -3,7 +3,6 @@ using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; 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> /// <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) public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string> requestHeaders = null)
{ {
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); SaveFilePath = ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); Uri = ArgumentValidator.EnsureNotNull(uri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1); WritePosition = ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath))) if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist."); throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new(); RequestHeaders = requestHeaders ?? new();
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
@ -109,8 +105,8 @@ namespace AaxDecrypter
#region Downloader #region Downloader
/// <summary> Update the <see cref="JsonFilePersister"/>. </summary> /// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
private void Update() private void OnUpdate()
{ {
RequestHeaders["Range"] = $"bytes={WritePosition}-"; RequestHeaders["Range"] = $"bytes={WritePosition}-";
try try
@ -167,7 +163,7 @@ namespace AaxDecrypter
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //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> /// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
@ -184,7 +180,7 @@ namespace AaxDecrypter
int bytesRead; int bytesRead;
do 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); await _writeFile.WriteAsync(buff, 0, bytesRead, _cancellationSource.Token);
downloadPosition += bytesRead; downloadPosition += bytesRead;
@ -193,7 +189,7 @@ namespace AaxDecrypter
{ {
await _writeFile.FlushAsync(_cancellationSource.Token); await _writeFile.FlushAsync(_cancellationSource.Token);
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update(); OnUpdate();
nextFlush = downloadPosition + DATA_FLUSH_SZ; nextFlush = downloadPosition + DATA_FLUSH_SZ;
_downloadedPiece.Set(); _downloadedPiece.Set();
} }
@ -233,19 +229,12 @@ namespace AaxDecrypter
networkStream.Close(); networkStream.Close();
_writeFile.Close(); _writeFile.Close();
_downloadedPiece.Set(); _downloadedPiece.Set();
Update(); OnUpdate();
} }
} }
#endregion #endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
=> new JsonSerializerSettings();
#endregion
#region Download Stream Reader #region Download Stream Reader
[JsonIgnore] [JsonIgnore]
@ -289,7 +278,7 @@ namespace AaxDecrypter
var toRead = Math.Min(count, Length - Position); var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead); 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) 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> /// <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) private void WaitToPosition(long requiredPosition)
{ {
while (WritePosition < requiredPosition while (WritePosition < requiredPosition
@ -317,20 +306,31 @@ namespace AaxDecrypter
} }
} }
public override void Close() private bool disposed = false;
/*
* 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(); _cancellationSource.Cancel();
_backgroundDownloadTask?.Wait(); _backgroundDownloadTask?.GetAwaiter().GetResult();
_downloadedPiece?.Dispose();
_cancellationSource?.Dispose();
_readFile.Dispose();
_writeFile.Dispose();
OnUpdate();
}
_readFile.Close(); disposed = true;
_writeFile.Close(); base.Dispose(disposing);
Update();
} }
#endregion #endregion
~NetworkFileStream()
{
_downloadedPiece?.Close();
}
} }
} }

View File

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

View File

@ -1,91 +1,29 @@
using System; using Dinah.Core.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core.Net.Http;
using FileManager; using FileManager;
using System;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
{ {
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic) public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic) { } : base(outFileName, cacheDirectory, dlLic)
public override async Task<bool> RunAsync()
{ {
try AsyncSteps.Name = "Download Unencrypted Audiobook";
{ AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
Serilog.Log.Information("Begin downloading unencrypted audiobook."); AsyncSteps["Step 2: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
//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;
}
} }
public override Task CancelAsync() public override Task CancelAsync()
{ {
IsCanceled = true; IsCanceled = true;
CloseInputFileStream(); FinalizeDownload();
return Task.CompletedTask; return Task.CompletedTask;
} }
protected bool Step_GetMetadata() protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
OnRetrievedCoverArt(null);
return !IsCanceled;
}
private bool Step_DownloadAudiobookAsSingleFile()
{ {
DateTime startTime = DateTime.Now; DateTime startTime = DateTime.Now;
@ -100,25 +38,28 @@ namespace AaxDecrypter
if (double.IsNormal(estTimeRemaining)) if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = (double)InputFileStream.WritePosition / InputFileStream.Length; var progressPercent = 100d * InputFileStream.WritePosition / InputFileStream.Length;
OnDecryptProgressUpdate( OnDecryptProgressUpdate(
new DownloadProgress new DownloadProgress
{ {
ProgressPercentage = 100 * progressPercent, ProgressPercentage = progressPercent,
BytesReceived = (long)(InputFileStream.Length * progressPercent), BytesReceived = InputFileStream.WritePosition,
TotalBytesToReceive = InputFileStream.Length TotalBytesToReceive = InputFileStream.Length
}); });
Thread.Sleep(200);
await Task.Delay(200);
} }
CloseInputFileStream(); if (IsCanceled)
return false;
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters); else
SetOutputFileName(realOutputFileName); {
OnFileCreated(realOutputFileName); FinalizeDownload();
FileUtility.SaferMove(InputFileStream.SaveFilePath, OutputFileName);
return !IsCanceled; OnFileCreated(OutputFileName);
return true;
}
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AaxDecrypter; using AaxDecrypter;
using ApplicationServices; using ApplicationServices;
using AudibleApi;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
@ -138,40 +137,26 @@ namespace FileLiberator
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic) private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
{ {
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3. //If DrmType != Adrm 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
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
? OutputFormat.Mp3
: OutputFormat.M4b;
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ? long chapterStartMs
OutputFormat.Mp3 : OutputFormat.M4b; = config.StripAudibleBrandAudio
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
: 0;
long chapterStartMs = config.StripAudibleBrandAudio ? var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl)
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
var dlOptions = new DownloadOptions
(
libraryBook,
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
Resources.USER_AGENT
)
{ {
AudibleKey = contentLic?.Voucher?.Key, AudibleKey = contentLic?.Voucher?.Key,
AudibleIV = contentLic?.Voucher?.Iv, AudibleIV = contentLic?.Voucher?.Iv,
OutputFormat = outputFormat, OutputFormat = outputFormat,
MoveMoovToBeginning = config.MoveMoovToBeginning,
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,
DownloadClipsBookmarks = config.DownloadClipsBookmarks,
DownloadSpeedBps = config.DownloadSpeedLimit,
LameConfig = GetLameOptions(config), LameConfig = GetLameOptions(config),
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
FixupFile = config.AllowLibationFixup RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
}; };
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList(); var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
@ -277,7 +262,9 @@ namespace FileLiberator
foreach (var c in chapters) foreach (var c in chapters)
{ {
if (c.Chapters is not null) if (c.Chapters is null)
chaps.Add(c);
else
{ {
if (c.LengthMs < 10000) if (c.LengthMs < 10000)
{ {
@ -296,8 +283,6 @@ namespace FileLiberator
chaps.AddRange(children); chaps.AddRange(children);
c.Chapters = null; c.Chapters = null;
} }
else
chaps.Add(c);
} }
return chaps; return chaps;
} }

View File

@ -3,7 +3,6 @@ using AAXClean;
using Dinah.Core; using Dinah.Core;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using FileManager;
using System.Threading.Tasks; using System.Threading.Tasks;
using System; using System;
using System.IO; using System.IO;
@ -17,36 +16,35 @@ namespace FileLiberator
public LibraryBook LibraryBook { get; } public LibraryBook LibraryBook { get; }
public LibraryBookDto LibraryBookDto { get; } public LibraryBookDto LibraryBookDto { get; }
public string DownloadUrl { get; } public string DownloadUrl { get; }
public string UserAgent { get; }
public string AudibleKey { get; init; } public string AudibleKey { get; init; }
public string AudibleIV { get; init; } public string AudibleIV { get; init; }
public AaxDecrypter.OutputFormat OutputFormat { get; init; } public TimeSpan RuntimeLength { get; init; }
public bool TrimOutputToChapterLength { get; init; } public OutputFormat OutputFormat { get; init; }
public bool RetainEncryptedFile { get; init; }
public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; }
public bool DownloadClipsBookmarks { get; init; }
public long DownloadSpeedBps { get; init; }
public ChapterInfo ChapterInfo { get; init; } public ChapterInfo ChapterInfo { get; init; }
public bool FixupFile { get; init; }
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
public bool Downsample { get; init; } public string UserAgent => AudibleApi.Resources.USER_AGENT;
public bool MatchSourceBitrate { get; init; } public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; public bool StripUnabridged => config.AllowLibationFixup && config.StripUnabridged;
public bool CreateCueSheet => config.CreateCueSheet;
public bool MoveMoovToBeginning { get; init; } public bool DownloadClipsBookmarks => config.DownloadClipsBookmarks;
public long DownloadSpeedBps => config.DownloadSpeedLimit;
public bool RetainEncryptedFile => config.RetainAaxFile;
public bool FixupFile => config.AllowLibationFixup;
public bool Downsample => config.AllowLibationFixup && config.LameDownsampleMono;
public bool MatchSourceBitrate => config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate;
public bool MoveMoovToBeginning => config.MoveMoovToBeginning;
public string GetMultipartFileName(MultiConvertFileProperties props) public string GetMultipartFileName(MultiConvertFileProperties props)
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props); => Templates.ChapterFile.GetFilename(LibraryBookDto, props);
public string GetMultipartTitleName(MultiConvertFileProperties props) public string GetMultipartTitle(MultiConvertFileProperties props)
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props); => Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
public async Task<string> SaveClipsAndBookmarks(string fileName) public async Task<string> SaveClipsAndBookmarksAsync(string fileName)
{ {
if (DownloadClipsBookmarks) if (DownloadClipsBookmarks)
{ {
var format = Configuration.Instance.ClipsBookmarksFileFormat; var format = config.ClipsBookmarksFileFormat;
var formatExtension = format.ToString().ToLowerInvariant(); var formatExtension = format.ToString().ToLowerInvariant();
var filePath = Path.ChangeExtension(fileName, formatExtension); var filePath = Path.ChangeExtension(fileName, formatExtension);
@ -71,20 +69,21 @@ namespace FileLiberator
return string.Empty; return string.Empty;
} }
private readonly Configuration config;
private readonly IDisposable cancellation; private readonly IDisposable cancellation;
public void Dispose() => cancellation?.Dispose(); public void Dispose() => cancellation?.Dispose();
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) public DownloadOptions(Configuration config, LibraryBook libraryBook, string downloadUrl)
{ {
this.config = ArgumentValidator.EnsureNotNull(config, nameof(config));
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them // no null/empty check for key/iv. unencrypted files do not have them
LibraryBookDto = LibraryBook.ToDto(); LibraryBookDto = LibraryBook.ToDto();
cancellation = cancellation =
Configuration.Instance config
.ObservePropertyChanged<long>( .ObservePropertyChanged<long>(
nameof(Configuration.DownloadSpeedLimit), nameof(Configuration.DownloadSpeedLimit),
newVal => DownloadSpeedChanged?.Invoke(this, newVal)); newVal => DownloadSpeedChanged?.Invoke(this, newVal));

View File

@ -40,6 +40,7 @@ namespace FileManager
} }
} }
[JsonConstructor]
private LongPath(string path) private LongPath(string path)
{ {
if (IsWindows && path.Length > MaxPathLength) if (IsWindows && path.Length > MaxPathLength)
@ -56,8 +57,7 @@ namespace FileManager
///a choice made by the linux kernel. As best as I can tell, pretty ///a choice made by the linux kernel. As best as I can tell, pretty
//much everyone uses UTF-8. //much everyone uses UTF-8.
public static int GetFilesystemStringLength(StringBuilder filename) public static int GetFilesystemStringLength(StringBuilder filename)
=> LongPath.IsWindows ? => IsWindows ? filename.Length
filename.Length
: Encoding.UTF8.GetByteCount(filename.ToString()); : Encoding.UTF8.GetByteCount(filename.ToString());
public static implicit operator LongPath(string path) public static implicit operator LongPath(string path)

View File

@ -2,7 +2,7 @@
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration : PropertyChangeFilter public partial class Configuration
{ {
/* /*
* Use this type in the getter for any Dictionary<TKey, TValue> settings, * Use this type in the getter for any Dictionary<TKey, TValue> settings,

View File

@ -7,7 +7,7 @@ using FileManager;
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration : PropertyChangeFilter
{ {
public bool LibationSettingsAreValid public bool LibationSettingsAreValid
=> File.Exists(APPSETTINGS_JSON) => File.Exists(APPSETTINGS_JSON)

View File

@ -1,326 +0,0 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace LibationFileManager
{
#region Useage
/*
* USEAGE
*************************
* *
* Event Filter Mode *
* *
*************************
propertyChangeFilter.PropertyChanged += MyPropertiesChanged;
[PropertyChangeFilter("MyProperty1")]
[PropertyChangeFilter("MyProperty2")]
void MyPropertiesChanged(object sender, PropertyChangedEventArgsEx e)
{
// Only properties whose names match either "MyProperty1"
// or "MyProperty2" will fire this event handler.
}
******
* OR *
******
propertyChangeFilter.PropertyChanged +=
[PropertyChangeFilter("MyProperty1")]
[PropertyChangeFilter("MyProperty2")]
(_, _) =>
{
// Only properties whose names match either "MyProperty1"
// or "MyProperty2" will fire this event handler.
};
*************************
* *
* Observable Mode *
* *
*************************
using var cancellation = propertyChangeFilter.ObservePropertyChanging<int>("MyProperty", MyPropertyChanging);
void MyPropertyChanging(int oldValue, int newValue)
{
// Only the property whose name match
// "MyProperty" will fire this method.
}
//The observer is delisted when cancellation is disposed
******
* OR *
******
using var cancellation = propertyChangeFilter.ObservePropertyChanged<bool>("MyProperty", s =>
{
// Only the property whose name match
// "MyProperty" will fire this action.
});
//The observer is delisted when cancellation is disposed
*/
#endregion
public abstract class PropertyChangeFilter
{
private readonly Dictionary<string, List<Delegate>> propertyChangedActions = new();
private readonly Dictionary<string, List<Delegate>> propertyChangingActions = new();
private readonly List<(PropertyChangedEventHandlerEx subscriber, PropertyChangedEventHandlerEx wrapper)> changedFilters = new();
private readonly List<(PropertyChangingEventHandlerEx subscriber, PropertyChangingEventHandlerEx wrapper)> changingFilters = new();
protected void OnPropertyChanged(string propertyName, object newValue)
{
if (propertyChangedActions.ContainsKey(propertyName))
{
//Invoke observables registered for propertyName
foreach (var action in propertyChangedActions[propertyName])
action.DynamicInvoke(newValue);
}
_propertyChanged?.Invoke(this, new(propertyName, newValue));
}
protected void OnPropertyChanging(string propertyName, object oldValue, object newValue)
{
if (propertyChangingActions.ContainsKey(propertyName))
{
//Invoke observables registered for propertyName
foreach (var action in propertyChangingActions[propertyName])
action.DynamicInvoke(oldValue, newValue);
}
_propertyChanging?.Invoke(this, new(propertyName, oldValue, newValue));
}
#region Events
private PropertyChangedEventHandlerEx _propertyChanged;
private PropertyChangingEventHandlerEx _propertyChanging;
public event PropertyChangedEventHandlerEx PropertyChanged
{
add
{
var attributes = getAttributes<PropertyChangeFilterAttribute>(value.Method);
if (attributes.Any())
{
var matches = attributes.Select(a => a.PropertyName).ToArray();
void filterer(object s, PropertyChangedEventArgsEx e)
{
if (e.PropertyName.In(matches)) value(s, e);
}
changedFilters.Add((value, filterer));
_propertyChanged += filterer;
}
else
_propertyChanged += value;
}
remove
{
var del = changedFilters.LastOrDefault(d => d.subscriber == value);
if (del == default)
_propertyChanged -= value;
else
{
_propertyChanged -= del.wrapper;
changedFilters.Remove(del);
}
}
}
public event PropertyChangingEventHandlerEx PropertyChanging
{
add
{
var attributes = getAttributes<PropertyChangeFilterAttribute>(value.Method);
if (attributes.Any())
{
var matches = attributes.Select(a => a.PropertyName).ToArray();
void filterer(object s, PropertyChangingEventArgsEx e)
{
if (e.PropertyName.In(matches)) value(s, e);
}
changingFilters.Add((value, filterer));
_propertyChanging += filterer;
}
else
_propertyChanging += value;
}
remove
{
var del = changingFilters.LastOrDefault(d => d.subscriber == value);
if (del == default)
_propertyChanging -= value;
else
{
_propertyChanging -= del.wrapper;
changingFilters.Remove(del);
}
}
}
private static T[] getAttributes<T>(MethodInfo methodInfo) where T : Attribute
=> Attribute.GetCustomAttributes(methodInfo, typeof(T)) as T[];
#endregion
#region Observables
/// <summary>
/// Clear all subscriptions to Property<b>Changed</b> for <paramref name="propertyName"/>
/// </summary>
public void ClearChangedSubscriptions(string propertyName)
{
if (propertyChangedActions.ContainsKey(propertyName)
&& propertyChangedActions[propertyName] is not null)
propertyChangedActions[propertyName].Clear();
}
/// <summary>
/// Clear all subscriptions to Property<b>Changing</b> for <paramref name="propertyName"/>
/// </summary>
public void ClearChangingSubscriptions(string propertyName)
{
if (propertyChangingActions.ContainsKey(propertyName)
&& propertyChangingActions[propertyName] is not null)
propertyChangingActions[propertyName].Clear();
}
/// <summary>
/// Add an action to be executed when a property's value has changed
/// </summary>
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
/// <param name="action">Action to be executed with the NewValue as a parameter</param>
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
public IDisposable ObservePropertyChanged<T>(string propertyName, Action<T> action)
{
validateSubscriber<T>(propertyName, action);
if (!propertyChangedActions.ContainsKey(propertyName))
propertyChangedActions.Add(propertyName, new List<Delegate>());
var actionlist = propertyChangedActions[propertyName];
if (!actionlist.Contains(action))
actionlist.Add(action);
return new Unsubscriber(actionlist, action);
}
/// <summary>
/// Add an action to be executed when a property's value is changing
/// </summary>
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
/// <param name="action">Action to be executed with OldValue and NewValue as parameters</param>
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
public IDisposable ObservePropertyChanging<T>(string propertyName, Action<T, T> action)
{
validateSubscriber<T>(propertyName, action);
if (!propertyChangingActions.ContainsKey(propertyName))
propertyChangingActions.Add(propertyName, new List<Delegate>());
var actionlist = propertyChangingActions[propertyName];
if (!actionlist.Contains(action))
actionlist.Add(action);
return new Unsubscriber(actionlist, action);
}
private void validateSubscriber<T>(string propertyName, Delegate action)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(propertyName, nameof(propertyName));
ArgumentValidator.EnsureNotNull(action, nameof(action));
var propertyInfo = GetType().GetProperty(propertyName);
if (propertyInfo is null)
throw new MissingMemberException($"{nameof(Configuration)}.{propertyName} does not exist.");
if (propertyInfo.PropertyType != typeof(T))
throw new InvalidCastException($"{nameof(Configuration)}.{propertyName} is {propertyInfo.PropertyType}, but parameter is {typeof(T)}.");
}
private class Unsubscriber : IDisposable
{
private List<Delegate> _observers;
private Delegate _observer;
internal Unsubscriber(List<Delegate> observers, Delegate observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
_observers.Remove(_observer);
}
}
#endregion
}
public delegate void PropertyChangedEventHandlerEx(object sender, PropertyChangedEventArgsEx e);
public delegate void PropertyChangingEventHandlerEx(object sender, PropertyChangingEventArgsEx e);
public class PropertyChangedEventArgsEx : PropertyChangedEventArgs
{
public object NewValue { get; }
public PropertyChangedEventArgsEx(string propertyName, object newValue) : base(propertyName)
{
NewValue = newValue;
}
}
public class PropertyChangingEventArgsEx : PropertyChangingEventArgs
{
public object OldValue { get; }
public object NewValue { get; }
public PropertyChangingEventArgsEx(string propertyName, object oldValue, object newValue) : base(propertyName)
{
OldValue = oldValue;
NewValue = newValue;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class PropertyChangeFilterAttribute : Attribute
{
public string PropertyName { get; }
public PropertyChangeFilterAttribute(string propertyName)
{
PropertyName = propertyName;
}
}
}