Refactor AaxDecrypter
This commit is contained in:
parent
4d6c742ae9
commit
25b37c6266
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,36 @@ 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 = 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;
|
||||||
|
|
||||||
|
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
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,17 @@ 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 string OutputFileName { get; private set; }
|
protected AsyncStepSequence AsyncSteps { get; } = new();
|
||||||
|
protected string OutputFileName { get; }
|
||||||
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 +44,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[$"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();
|
public abstract Task CancelAsync();
|
||||||
|
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||||
|
|
||||||
public virtual void SetCoverArt(byte[] coverArt)
|
public virtual void SetCoverArt(byte[] coverArt)
|
||||||
{
|
{
|
||||||
@ -62,8 +84,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,13 +99,25 @@ 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 (!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;
|
if (!DownloadOptions.CreateCueSheet) return true;
|
||||||
|
|
||||||
@ -93,56 +125,41 @@ namespace AaxDecrypter
|
|||||||
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;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task<bool> Step_DownloadClipsBookmarks()
|
|
||||||
{
|
|
||||||
if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks)
|
|
||||||
{
|
|
||||||
var recordsFile = await DownloadOptions.SaveClipsAndBookmarks(OutputFileName);
|
|
||||||
|
|
||||||
if (File.Exists(recordsFile))
|
|
||||||
OnFileCreated(recordsFile);
|
|
||||||
}
|
|
||||||
return !IsCanceled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private NetworkFileStreamPersister OpenNetworkFileStream()
|
private NetworkFileStreamPersister OpenNetworkFileStream()
|
||||||
@ -151,31 +168,31 @@ 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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using NAudio.Lame;
|
using NAudio.Lame;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using FileManager;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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]
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,10 @@ 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)
|
||||||
|
{
|
||||||
|
NetworkFileStream?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
@ -109,14 +47,17 @@ namespace AaxDecrypter
|
|||||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||||
TotalBytesToReceive = InputFileStream.Length
|
TotalBytesToReceive = InputFileStream.Length
|
||||||
});
|
});
|
||||||
Thread.Sleep(200);
|
|
||||||
|
await Task.Delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
CloseInputFileStream();
|
FinalizeDownload();
|
||||||
|
|
||||||
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters);
|
if (!IsCanceled)
|
||||||
SetOutputFileName(realOutputFileName);
|
{
|
||||||
OnFileCreated(realOutputFileName);
|
FileUtility.SaferMove(InputFileStream.SaveFilePath, OutputFileName);
|
||||||
|
OnFileCreated(OutputFileName);
|
||||||
|
}
|
||||||
|
|
||||||
return !IsCanceled;
|
return !IsCanceled;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="7.2.0.1" />
|
<PackageReference Include="Dinah.Core" Version="7.2.1.1" />
|
||||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.0.1" />
|
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="7.2.0.1" />
|
<PackageReference Include="Dinah.Core" Version="7.2.1.1" />
|
||||||
<PackageReference Include="Polly" Version="7.2.3" />
|
<PackageReference Include="Polly" Version="7.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user