diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs index 994c07ae..e7b46016 100644 --- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -1,7 +1,7 @@ -using System; -using System.Threading.Tasks; -using AAXClean; +using AAXClean; using Dinah.Core.Net.Http; +using System; +using System.Threading.Tasks; namespace AaxDecrypter { @@ -9,8 +9,23 @@ namespace AaxDecrypter { public event EventHandler RetrievedMetadata; - protected AaxFile AaxFile; - protected Mp4Operation aaxConversion; + protected AaxFile AaxFile { get; private set; } + private Mp4Operation aaxConversion; + protected Mp4Operation AaxConversion + { + get => aaxConversion; + set + { + if (aaxConversion is not null) + aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; + + if (value is not null) + { + aaxConversion = value; + aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; + } + } + } protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) : base(outFileName, cacheDirectory, dlOptions) { } @@ -23,9 +38,23 @@ namespace AaxDecrypter AaxFile.AppleTags.Cover = coverArt; } + public override async Task CancelAsync() + { + IsCanceled = true; + await (AaxConversion?.CancelAsync() ?? Task.CompletedTask); + FinalizeDownload(); + } + + protected override void FinalizeDownload() + { + AaxConversion = null; + base.FinalizeDownload(); + } + protected bool Step_GetMetadata() { AaxFile = new AaxFile(InputFileStream); + AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV); if (DownloadOptions.StripUnabridged) { @@ -44,7 +73,6 @@ namespace AaxDecrypter DownloadOptions.Downsample, DownloadOptions.MatchSourceBitrate); - OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged); OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]"); OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]"); @@ -55,40 +83,15 @@ namespace AaxDecrypter return !IsCanceled; } - protected DownloadProgress Step_DownloadAudiobook_Start() + private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) { - var zeroProgress = new DownloadProgress - { - BytesReceived = 0, - ProgressPercentage = 0, - TotalBytesToReceive = InputFileStream.Length - }; - - OnDecryptProgressUpdate(zeroProgress); - - AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV); - return zeroProgress; - } - - protected void Step_DownloadAudiobook_End(DownloadProgress zeroProgress) - { - AaxFile.Close(); - - CloseInputFileStream(); - - OnDecryptProgressUpdate(zeroProgress); - } - - protected void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) - { - var duration = AaxFile.Duration; - var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds; + var remainingSecsToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds; var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed; if (double.IsNormal(estTimeRemaining)) OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); - var progressPercent = (e.ProcessPosition / e.TotalDuration); + var progressPercent = e.ProcessPosition / e.TotalDuration; OnDecryptProgressUpdate( new DownloadProgress @@ -98,14 +101,5 @@ namespace AaxDecrypter TotalBytesToReceive = InputFileStream.Length }); } - - public override async Task CancelAsync() - { - IsCanceled = true; - if (aaxConversion != null) - await aaxConversion.CancelAsync(); - AaxFile?.Close(); - CloseInputFileStream(); - } } } diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index 8b198665..2e2fb1fd 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -1,81 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using AAXClean; +using AAXClean; using AAXClean.Codecs; using FileManager; +using System; +using System.IO; +using System.Threading.Tasks; namespace AaxDecrypter { public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase { - private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3); - private List multiPartFilePaths { get; } = new List(); + private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3); private FileStream workingFileStream; public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) - : base(outFileName, cacheDirectory, dlOptions) { } - - public override async Task RunAsync() + : base(outFileName, cacheDirectory, dlOptions) { - try - { - Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - - //Step 1 - Serilog.Log.Information("Begin Get Aaxc Metadata"); - if (await Task.Run(Step_GetMetadata)) - Serilog.Log.Information("Completed Get Aaxc Metadata"); - else - { - Serilog.Log.Information("Failed to Complete Get Aaxc Metadata"); - return false; - } - - //Step 2 - Serilog.Log.Information("Begin Download Decrypted Audiobook"); - if (await Step_DownloadAudiobookAsMultipleFilesPerChapter()) - Serilog.Log.Information("Completed Download Decrypted Audiobook"); - else - { - Serilog.Log.Information("Failed to Complete Download Decrypted Audiobook"); - return false; - } - - - //Step 3 - if (DownloadOptions.DownloadClipsBookmarks) - { - Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); - if (await Task.Run(Step_DownloadClipsBookmarks)) - Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); - else - { - Serilog.Log.Information("Failed to Download Clips and Bookmarks"); - return false; - } - } - - //Step 4 - Serilog.Log.Information("Begin Cleanup"); - if (await Task.Run(Step_Cleanup)) - Serilog.Log.Information("Completed Cleanup"); - else - { - Serilog.Log.Information("Failed to Complete Cleanup"); - return false; - } - - Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return true; - } - catch (Exception ex) - { - Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return false; - } + AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split"; + AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata); + AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync; + AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync; } /* @@ -102,10 +45,8 @@ The book will be split into the following files: That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name. */ - private async Task Step_DownloadAudiobookAsMultipleFilesPerChapter() + protected async override Task Step_DownloadAndDecryptAudiobookAsync() { - var zeroProgress = Step_DownloadAudiobook_Start(); - var chapters = DownloadOptions.ChapterInfo.Chapters; // Ensure split files are at least minChapterLength in duration. @@ -128,110 +69,81 @@ That naming may not be desirable for everyone, but it's an easy change to instea } } - // reset, just in case - multiPartFilePaths.Clear(); - try { - if (DownloadOptions.OutputFormat == OutputFormat.M4b) - aaxConversion = ConvertToMultiMp4a(splitChapters); - else - aaxConversion = ConvertToMultiMp3(splitChapters); + await (AaxConversion = decryptMultiAsync(splitChapters)); - aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; - await aaxConversion; + if (AaxConversion.IsCompletedSuccessfully) + await moveMoovToBeginning(workingFileStream?.Name); - if (aaxConversion.IsCompletedSuccessfully) - moveMoovToBeginning(workingFileStream?.Name); - - return aaxConversion.IsCompletedSuccessfully; - } - catch(Exception ex) - { - Serilog.Log.Error(ex, "AAXClean Error"); - workingFileStream?.Close(); - if (workingFileStream?.Name is not null) - FileUtility.SaferDelete(workingFileStream.Name); - return false; + return AaxConversion.IsCompletedSuccessfully; } finally { - if (aaxConversion is not null) - aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; - - Step_DownloadAudiobook_End(zeroProgress); + workingFileStream?.Dispose(); + FinalizeDownload(); } } - private Mp4Operation ConvertToMultiMp4a(ChapterInfo splitChapters) + private Mp4Operation decryptMultiAsync(ChapterInfo splitChapters) { var chapterCount = 0; - return AaxFile.ConvertToMultiMp4aAsync + return + DownloadOptions.OutputFormat == OutputFormat.M4b + ? AaxFile.ConvertToMultiMp4aAsync ( splitChapters, - newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), + newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback), DownloadOptions.TrimOutputToChapterLength - ); - } - - private Mp4Operation ConvertToMultiMp3(ChapterInfo splitChapters) - { - var chapterCount = 0; - return AaxFile.ConvertToMultiMp3Async + ) + : AaxFile.ConvertToMultiMp3Async ( splitChapters, - newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), + newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback), DownloadOptions.LameConfig, DownloadOptions.TrimOutputToChapterLength ); - } - - private void Callback(int currentChapter, ChapterInfo splitChapters, NewMP3SplitCallback newSplitCallback) - => Callback(currentChapter, splitChapters, newSplitCallback as NewSplitCallback); - - private void Callback(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback) - { - MultiConvertFileProperties props = new() + void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback) { - OutputFileName = OutputFileName, - PartsPosition = currentChapter, - PartsTotal = splitChapters.Count, - Title = newSplitCallback?.Chapter?.Title, - }; + MultiConvertFileProperties props = new() + { + OutputFileName = OutputFileName, + PartsPosition = currentChapter, + PartsTotal = splitChapters.Count, + Title = newSplitCallback?.Chapter?.Title, + }; - moveMoovToBeginning(workingFileStream?.Name); + moveMoovToBeginning(workingFileStream?.Name).GetAwaiter().GetResult(); - newSplitCallback.OutputFile = createOutputFileStream(props); - newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitleName(props); - newSplitCallback.TrackNumber = currentChapter; - newSplitCallback.TrackCount = splitChapters.Count; + newSplitCallback.OutputFile = createOutputFileStream(props); + newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props); + newSplitCallback.TrackNumber = currentChapter; + newSplitCallback.TrackCount = splitChapters.Count; + + FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) + { + var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); + FileUtility.SaferDelete(fileName); + + workingFileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); + OnFileCreated(fileName); + + return workingFileStream; + } + } } - private void moveMoovToBeginning(string filename) + private Mp4Operation moveMoovToBeginning(string filename) { if (DownloadOptions.OutputFormat is OutputFormat.M4b && DownloadOptions.MoveMoovToBeginning && filename is not null && File.Exists(filename)) { - Mp4File.RelocateMoovAsync(filename).GetAwaiter().GetResult(); + return Mp4File.RelocateMoovAsync(filename); } - } - - private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) - { - var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); - var extension = Path.GetExtension(fileName); - fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters, extension); - - multiPartFilePaths.Add(fileName); - - FileUtility.SaferDelete(fileName); - - workingFileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); - OnFileCreated(fileName); - return workingFileStream; + else return Mp4Operation.CompletedOperation; } } } diff --git a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs index bc549b44..335b62e9 100644 --- a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs @@ -1,153 +1,67 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using AAXClean; +using AAXClean; using AAXClean.Codecs; using FileManager; -using Mpeg4Lib.Util; +using System.IO; +using System.Threading.Tasks; namespace AaxDecrypter { public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase { public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) - : base(outFileName, cacheDirectory, dlOptions) { } - - public override async Task RunAsync() + : base(outFileName, cacheDirectory, dlOptions) { - try - { - Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - //Step 1 - Serilog.Log.Information("Begin Step 1: Get Aaxc Metadata"); - if (await Task.Run(Step_GetMetadata)) - Serilog.Log.Information("Completed Step 1: Get Aaxc Metadata"); - else - { - Serilog.Log.Information("Failed to Complete Step 1: Get Aaxc Metadata"); - return false; - } - - //Step 2 - Serilog.Log.Information("Begin Step 2: Download Decrypted Audiobook"); - if (await Step_DownloadAudiobookAsSingleFile()) - Serilog.Log.Information("Completed Step 2: Download Decrypted Audiobook"); - else - { - Serilog.Log.Information("Failed to Complete Step 2: Download Decrypted Audiobook"); - return false; - } - - //Step 3 - Serilog.Log.Information("Begin Step 3: Create Cue"); - if (await Task.Run(Step_CreateCue)) - Serilog.Log.Information("Completed Step 3: Create Cue"); - else - { - Serilog.Log.Information("Failed to Complete Step 3: Create Cue"); - return false; - } - - //Step 4 - if (DownloadOptions.DownloadClipsBookmarks) - { - Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); - if (await Task.Run(Step_DownloadClipsBookmarks)) - Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); - else - { - Serilog.Log.Information("Failed to Download Clips and Bookmarks"); - return false; - } - } - - //Step 5 - Serilog.Log.Information("Begin Step 4: Cleanup"); - if (await Task.Run(Step_Cleanup)) - Serilog.Log.Information("Completed Step 4: Cleanup"); - else - { - Serilog.Log.Information("Failed to Complete Step 4: Cleanup"); - return false; - } - - Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return true; - } - catch (Exception ex) - { - Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return false; - } + AsyncSteps.Name = $"Download and Convert Aaxc To {DownloadOptions.OutputFormat}"; + AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata); + AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync; + AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync; + AsyncSteps["Step 4: Create Cue"] = Step_CreateCueAsync; } - private async Task Step_DownloadAudiobookAsSingleFile() + protected async override Task Step_DownloadAndDecryptAudiobookAsync() { - var zeroProgress = Step_DownloadAudiobook_Start(); - FileUtility.SaferDelete(OutputFileName); - var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); + using var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); OnFileCreated(OutputFileName); - try { - aaxConversion = decryptAsync(outputFile); - aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; - await aaxConversion; + await (AaxConversion = decryptAsync(outputFile)); - outputFile.Close(); - - if (aaxConversion.IsCompletedSuccessfully - && DownloadOptions.OutputFormat is OutputFormat.M4b - && DownloadOptions.MoveMoovToBeginning) + if (AaxConversion.IsCompletedSuccessfully + && DownloadOptions.MoveMoovToBeginning + && DownloadOptions.OutputFormat is OutputFormat.M4b) { - aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; - aaxConversion = Mp4File.RelocateMoovAsync(OutputFileName); - aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; - await aaxConversion; + outputFile.Close(); + await (AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName)); } - if (aaxConversion.IsCompletedSuccessfully) - base.OnFileCreated(OutputFileName); - - return aaxConversion.IsCompletedSuccessfully; - } - catch(Exception ex) - { - Serilog.Log.Error(ex, "AAXClean Error"); - FileUtility.SaferDelete(OutputFileName); - return false; + return AaxConversion.IsCompletedSuccessfully; } finally { - outputFile.Close(); - - if (aaxConversion is not null) - aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; - - Step_DownloadAudiobook_End(zeroProgress); + FinalizeDownload(); } } private Mp4Operation decryptAsync(Stream outputFile) - => DownloadOptions.OutputFormat == OutputFormat.Mp3 ? - AaxFile.ConvertToMp3Async + => DownloadOptions.OutputFormat == OutputFormat.Mp3 + ? AaxFile.ConvertToMp3Async ( outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength ) - : DownloadOptions.FixupFile ? - AaxFile.ConvertToMp4aAsync - ( - outputFile, - DownloadOptions.ChapterInfo, - DownloadOptions.TrimOutputToChapterLength - ) - : AaxFile.ConvertToMp4aAsync(outputFile); + : DownloadOptions.FixupFile + ? AaxFile.ConvertToMp4aAsync + ( + outputFile, + DownloadOptions.ChapterInfo, + DownloadOptions.TrimOutputToChapterLength + ) + : AaxFile.ConvertToMp4aAsync(outputFile); } } diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 622142fe..90bcb2df 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -1,9 +1,10 @@ -using System; +using Dinah.Core; +using Dinah.Core.Net.Http; +using Dinah.Core.StepRunner; +using FileManager; +using System; using System.IO; using System.Threading.Tasks; -using Dinah.Core; -using Dinah.Core.Net.Http; -using FileManager; namespace AaxDecrypter { @@ -19,19 +20,17 @@ namespace AaxDecrypter public event EventHandler DecryptTimeRemaining; public event EventHandler FileCreated; - public bool IsCanceled { get; set; } - public string TempFilePath { get; } + public bool IsCanceled { get; protected set; } - protected string OutputFileName { get; private set; } + protected AsyncStepSequence AsyncSteps { get; } = new(); + protected string OutputFileName { get; } protected IDownloadOptions DownloadOptions { get; } - protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream; + protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream; - // Don't give the property a 'set'. This should have to be an obvious choice; not accidental - protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName; - - private NetworkFileStreamPersister nfsPersister; - - private string jsonDownloadState { get; } + private readonly NetworkFileStreamPersister nfsPersister; + private readonly DownloadProgress zeroProgress; + private readonly string jsonDownloadState; + private readonly string tempFilePath; protected AudiobookDownloadBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) { @@ -45,16 +44,39 @@ namespace AaxDecrypter Directory.CreateDirectory(cacheDirectory); jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json"))); - TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); + tempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions)); DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed; // delete file after validation is complete FileUtility.SaferDelete(OutputFileName); + + nfsPersister = OpenNetworkFileStream(); + + zeroProgress = new DownloadProgress + { + BytesReceived = 0, + ProgressPercentage = 0, + TotalBytesToReceive = InputFileStream.Length + }; + + OnDecryptProgressUpdate(zeroProgress); + } + + public async Task RunAsync() + { + AsyncSteps[$"Final Step: Cleanup"] = CleanupAsync; + (bool success, var elapsed) = await AsyncSteps.RunAsync(); + + var speedup = DownloadOptions.RuntimeLength.TotalSeconds / elapsed.TotalSeconds; + Serilog.Log.Information($"Speedup is {speedup:F0}x realtime."); + + return success; } public abstract Task CancelAsync(); + protected abstract Task Step_DownloadAndDecryptAudiobookAsync(); public virtual void SetCoverArt(byte[] coverArt) { @@ -62,8 +84,6 @@ namespace AaxDecrypter OnRetrievedCoverArt(coverArt); } - public abstract Task RunAsync(); - protected void OnRetrievedTitle(string title) => RetrievedTitle?.Invoke(this, title); protected void OnRetrievedAuthors(string authors) @@ -79,13 +99,25 @@ namespace AaxDecrypter protected void OnFileCreated(string path) => FileCreated?.Invoke(this, path); - protected void CloseInputFileStream() + protected virtual void FinalizeDownload() { - nfsPersister?.NetworkFileStream?.Close(); nfsPersister?.Dispose(); + OnDecryptProgressUpdate(zeroProgress); } - protected bool Step_CreateCue() + protected async Task Step_DownloadClipsBookmarksAsync() + { + if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks) + { + var recordsFile = await DownloadOptions.SaveClipsAndBookmarksAsync(OutputFileName); + + if (File.Exists(recordsFile)) + OnFileCreated(recordsFile); + } + return !IsCanceled; + } + + protected async Task Step_CreateCueAsync() { if (!DownloadOptions.CreateCueSheet) return true; @@ -93,56 +125,41 @@ namespace AaxDecrypter try { var path = Path.ChangeExtension(OutputFileName, ".cue"); - path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters, ".cue"); - File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); + await File.WriteAllTextAsync(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); OnFileCreated(path); } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCue)}. FAILED"); + Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCueAsync)} Failed"); } return !IsCanceled; } - protected bool Step_Cleanup() + private async Task CleanupAsync() { - bool success = !IsCanceled; - if (success) + if (IsCanceled) return false; + + FileUtility.SaferDelete(jsonDownloadState); + + if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) && + !string.IsNullOrEmpty(DownloadOptions.AudibleIV) && + DownloadOptions.RetainEncryptedFile) { - FileUtility.SaferDelete(jsonDownloadState); + string aaxPath = Path.ChangeExtension(tempFilePath, ".aax"); + FileUtility.SaferMove(tempFilePath, aaxPath); - if (DownloadOptions.AudibleKey is not null && - DownloadOptions.AudibleIV is not null && - DownloadOptions.RetainEncryptedFile) - { - string aaxPath = Path.ChangeExtension(TempFilePath, ".aax"); - FileUtility.SaferMove(TempFilePath, aaxPath); + //Write aax decryption key + string keyPath = Path.ChangeExtension(aaxPath, ".key"); + FileUtility.SaferDelete(keyPath); + await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}"); - //Write aax decryption key - string keyPath = Path.ChangeExtension(aaxPath, ".key"); - FileUtility.SaferDelete(keyPath); - File.WriteAllText(keyPath, $"Key={DownloadOptions.AudibleKey}\r\nIV={DownloadOptions.AudibleIV}"); - - OnFileCreated(aaxPath); - OnFileCreated(keyPath); - } - else - FileUtility.SaferDelete(TempFilePath); + OnFileCreated(aaxPath); + OnFileCreated(keyPath); } + else + FileUtility.SaferDelete(tempFilePath); - return success; - } - - protected async Task Step_DownloadClipsBookmarks() - { - if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks) - { - var recordsFile = await DownloadOptions.SaveClipsAndBookmarks(OutputFileName); - - if (File.Exists(recordsFile)) - OnFileCreated(recordsFile); - } - return !IsCanceled; + return true; } private NetworkFileStreamPersister OpenNetworkFileStream() @@ -151,31 +168,31 @@ namespace AaxDecrypter try { if (!File.Exists(jsonDownloadState)) - return nfsp = NewNetworkFilePersister(); + return nfsp = newNetworkFilePersister(); nfsp = new NetworkFileStreamPersister(jsonDownloadState); - // If More than ~1 hour has elapsed since getting the download url, it will expire. - // The new url will be to the same file. + // The download url expires after 1 hour. + // The new url points to the same file. nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl)); return nfsp; } catch { FileUtility.SaferDelete(jsonDownloadState); - FileUtility.SaferDelete(TempFilePath); - return nfsp = NewNetworkFilePersister(); + FileUtility.SaferDelete(tempFilePath); + return nfsp = newNetworkFilePersister(); } finally { if (nfsp?.NetworkFileStream is not null) nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps; } - } - private NetworkFileStreamPersister NewNetworkFilePersister() - { - var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } }); - return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); + NetworkFileStreamPersister newNetworkFilePersister() + { + var networkFileStream = new NetworkFileStream(tempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } }); + return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); + } } } } diff --git a/Source/AaxDecrypter/Cue.cs b/Source/AaxDecrypter/Cue.cs index 624895ce..af9a0730 100644 --- a/Source/AaxDecrypter/Cue.cs +++ b/Source/AaxDecrypter/Cue.cs @@ -1,62 +1,60 @@ -using System; +using AAXClean; +using Dinah.Core; using System.IO; using System.Text; -using AAXClean; -using Dinah.Core; namespace AaxDecrypter { - public static class Cue - { - public static string CreateContents(string filePath, ChapterInfo chapters) - { - var stringBuilder = new StringBuilder(); + public static class Cue + { + public static string CreateContents(string filePath, ChapterInfo chapters) + { + var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine(GetFileLine(filePath, "MP3")); + stringBuilder.AppendLine(GetFileLine(filePath, "MP3")); - var startOffset = chapters.StartOffset; + var startOffset = chapters.StartOffset; - var trackCount = 0; - foreach (var c in chapters.Chapters) - { - var startTime = c.StartOffset - startOffset; - trackCount++; + var trackCount = 1; + foreach (var c in chapters.Chapters) + { + var startTime = c.StartOffset - startOffset; - stringBuilder.AppendLine($"TRACK {trackCount} AUDIO"); - stringBuilder.AppendLine($" TITLE \"{c.Title}\""); - stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}"); - } + stringBuilder.AppendLine($"TRACK {trackCount++} AUDIO"); + stringBuilder.AppendLine($" TITLE \"{c.Title}\""); + stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds * 75d / 1000):D2}"); + } - return stringBuilder.ToString(); - } + return stringBuilder.ToString(); + } - public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath) - => UpdateFileName(cueFileInfo.FullName, audioFilePath); + public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath) + => UpdateFileName(cueFileInfo.FullName, audioFilePath); - public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo) - => UpdateFileName(cueFilePath, audioFileInfo.FullName); + public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo) + => UpdateFileName(cueFilePath, audioFileInfo.FullName); - public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo) - => UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName); + public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo) + => UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName); - public static void UpdateFileName(string cueFilePath, string audioFilePath) - { - var cueContents = File.ReadAllLines(cueFilePath); + public static void UpdateFileName(string cueFilePath, string audioFilePath) + { + var cueContents = File.ReadAllLines(cueFilePath); - for (var i = 0; i < cueContents.Length; i++) - { - var line = cueContents[i]; - if (!line.Trim().StartsWith("FILE") || !line.Contains(" ")) - continue; + for (var i = 0; i < cueContents.Length; i++) + { + var line = cueContents[i]; + if (!line.Trim().StartsWith("FILE") || !line.Contains(' ')) + continue; - var fileTypeBegins = line.LastIndexOf(" ") + 1; - cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]); - break; - } + var fileTypeBegins = line.LastIndexOf(" ") + 1; + cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]); + break; + } - File.WriteAllLines(cueFilePath, cueContents); - } + File.WriteAllLines(cueFilePath, cueContents); + } - private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}"; - } + private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}"; + } } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index f67e2040..ccbb3808 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -5,13 +5,13 @@ using System.Threading.Tasks; namespace AaxDecrypter { public interface IDownloadOptions - { + { event EventHandler DownloadSpeedChanged; - FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } string UserAgent { get; } string AudibleKey { get; } string AudibleIV { get; } + TimeSpan RuntimeLength { get; } OutputFormat OutputFormat { get; } bool TrimOutputToChapterLength { get; } bool RetainEncryptedFile { get; } @@ -26,7 +26,7 @@ namespace AaxDecrypter bool MatchSourceBitrate { get; } bool MoveMoovToBeginning { get; } string GetMultipartFileName(MultiConvertFileProperties props); - string GetMultipartTitleName(MultiConvertFileProperties props); - Task SaveClipsAndBookmarks(string fileName); - } + string GetMultipartTitle(MultiConvertFileProperties props); + Task SaveClipsAndBookmarksAsync(string fileName); + } } diff --git a/Source/AaxDecrypter/MpegUtil.cs b/Source/AaxDecrypter/MpegUtil.cs index 116725b4..4d35fd2d 100644 --- a/Source/AaxDecrypter/MpegUtil.cs +++ b/Source/AaxDecrypter/MpegUtil.cs @@ -1,7 +1,5 @@ using AAXClean; using NAudio.Lame; -using System; -using System.Linq; namespace AaxDecrypter { diff --git a/Source/AaxDecrypter/MultiConvertFileProperties.cs b/Source/AaxDecrypter/MultiConvertFileProperties.cs index 1618884b..58fe335a 100644 --- a/Source/AaxDecrypter/MultiConvertFileProperties.cs +++ b/Source/AaxDecrypter/MultiConvertFileProperties.cs @@ -1,15 +1,13 @@ using System; -using System.IO; -using FileManager; namespace AaxDecrypter { - public class MultiConvertFileProperties - { - public string OutputFileName { get; set; } - public int PartsPosition { get; set; } - public int PartsTotal { get; set; } - public string Title { get; set; } - public DateTime FileDate { get; } = DateTime.Now; - } + public class MultiConvertFileProperties + { + public string OutputFileName { get; set; } + public int PartsPosition { get; set; } + public int PartsTotal { get; set; } + public string Title { get; set; } + public DateTime FileDate { get; } = DateTime.Now; + } } diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 23b7b19a..e0ece8e4 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -83,16 +82,13 @@ namespace AaxDecrypter /// Http headers to be sent to the server with the . public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary requestHeaders = null) { - ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); - ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); - ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1); + SaveFilePath = ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); + Uri = ArgumentValidator.EnsureNotNull(uri, nameof(uri)); + WritePosition = ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1); if (!Directory.Exists(Path.GetDirectoryName(saveFilePath))) throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist."); - SaveFilePath = saveFilePath; - Uri = uri; - WritePosition = writePosition; RequestHeaders = requestHeaders ?? new(); _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) @@ -109,8 +105,8 @@ namespace AaxDecrypter #region Downloader - /// Update the . - private void Update() + /// Update the . + private void OnUpdate() { RequestHeaders["Range"] = $"bytes={WritePosition}-"; try @@ -167,7 +163,7 @@ namespace AaxDecrypter _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); //Download the file in the background. - return Task.Run(async () => await DownloadFile(networkStream), _cancellationSource.Token); + return Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token); } /// Download to . @@ -184,7 +180,7 @@ namespace AaxDecrypter int bytesRead; do { - bytesRead = await networkStream.ReadAsync(buff, 0, DOWNLOAD_BUFF_SZ, _cancellationSource.Token); + bytesRead = await networkStream.ReadAsync(buff, _cancellationSource.Token); await _writeFile.WriteAsync(buff, 0, bytesRead, _cancellationSource.Token); downloadPosition += bytesRead; @@ -193,7 +189,7 @@ namespace AaxDecrypter { await _writeFile.FlushAsync(_cancellationSource.Token); WritePosition = downloadPosition; - Update(); + OnUpdate(); nextFlush = downloadPosition + DATA_FLUSH_SZ; _downloadedPiece.Set(); } @@ -233,19 +229,12 @@ namespace AaxDecrypter networkStream.Close(); _writeFile.Close(); _downloadedPiece.Set(); - Update(); + OnUpdate(); } } #endregion - #region Json Connverters - - public static JsonSerializerSettings GetJsonSerializerSettings() - => new JsonSerializerSettings(); - - #endregion - #region Download Stream Reader [JsonIgnore] @@ -289,7 +278,7 @@ namespace AaxDecrypter var toRead = Math.Min(count, Length - Position); WaitToPosition(Position + toRead); - return IsCancelled ? 0: _readFile.Read(buffer, offset, count); + return IsCancelled ? 0 : _readFile.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) @@ -306,7 +295,7 @@ namespace AaxDecrypter } /// Blocks until the file has downloaded to at least , then returns. - /// The minimum required flished data length in . + /// The minimum required flushed data length in . private void WaitToPosition(long requiredPosition) { while (WritePosition < requiredPosition @@ -317,20 +306,31 @@ namespace AaxDecrypter } } - public override void Close() - { - _cancellationSource.Cancel(); - _backgroundDownloadTask?.Wait(); + private bool disposed = false; - _readFile.Close(); - _writeFile.Close(); - Update(); + /* + * https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.dispose?view=net-7.0 + * + * In derived classes, do not override the Close() method, instead, put all of the + * Stream cleanup logic in the Dispose(Boolean) method. + */ + protected override void Dispose(bool disposing) + { + if (disposing && !disposed) + { + _cancellationSource.Cancel(); + _backgroundDownloadTask?.GetAwaiter().GetResult(); + _downloadedPiece?.Dispose(); + _cancellationSource?.Dispose(); + _readFile.Dispose(); + _writeFile.Dispose(); + OnUpdate(); + } + + disposed = true; + base.Dispose(disposing); } #endregion - ~NetworkFileStream() - { - _downloadedPiece?.Close(); - } } } diff --git a/Source/AaxDecrypter/NetworkFileStreamPersister.cs b/Source/AaxDecrypter/NetworkFileStreamPersister.cs index 38fe3244..a9c2d254 100644 --- a/Source/AaxDecrypter/NetworkFileStreamPersister.cs +++ b/Source/AaxDecrypter/NetworkFileStreamPersister.cs @@ -1,11 +1,9 @@ using Dinah.Core.IO; -using Newtonsoft.Json; namespace AaxDecrypter { - internal class NetworkFileStreamPersister : JsonFilePersister - { - + internal class NetworkFileStreamPersister : JsonFilePersister + { /// Alias for Target public NetworkFileStream NetworkFileStream => Target; @@ -17,7 +15,10 @@ namespace AaxDecrypter public NetworkFileStreamPersister(string path, string jsonPath = null) : base(path, jsonPath) { } - protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings(); - - } + protected override void Dispose(bool disposing) + { + NetworkFileStream?.Dispose(); + base.Dispose(disposing); + } + } } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index e9c062f0..64580c6d 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -1,97 +1,35 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Dinah.Core.Net.Http; +using Dinah.Core.Net.Http; using FileManager; +using System; +using System.Threading.Tasks; namespace AaxDecrypter { public class UnencryptedAudiobookDownloader : AudiobookDownloadBase { - public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic) - : base(outFileName, cacheDirectory, dlLic) { } - - public override async Task RunAsync() + : base(outFileName, cacheDirectory, dlLic) { - try - { - Serilog.Log.Information("Begin downloading unencrypted audiobook."); - - //Step 1 - Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata"); - if (await Task.Run(Step_GetMetadata)) - Serilog.Log.Information("Completed Step 1: Get Mp3 Metadata"); - else - { - Serilog.Log.Information("Failed to Complete Step 1: Get Mp3 Metadata"); - return false; - } - - //Step 2 - Serilog.Log.Information("Begin Step 2: Download Audiobook"); - if (await Task.Run(Step_DownloadAudiobookAsSingleFile)) - Serilog.Log.Information("Completed Step 2: Download Audiobook"); - else - { - Serilog.Log.Information("Failed to Complete Step 2: Download Audiobook"); - return false; - } - - //Step 3 - if (DownloadOptions.DownloadClipsBookmarks) - { - Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); - if (await Task.Run(Step_DownloadClipsBookmarks)) - Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); - else - { - Serilog.Log.Information("Failed to Download Clips and Bookmarks"); - return false; - } - } - - //Step 4 - Serilog.Log.Information("Begin Step 3: Cleanup"); - if (await Task.Run(Step_Cleanup)) - Serilog.Log.Information("Completed Step 3: Cleanup"); - else - { - Serilog.Log.Information("Failed to Complete Step 3: Cleanup"); - return false; - } - - Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return true; - } - catch (Exception ex) - { - Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); - return false; - } + AsyncSteps.Name = "Download Unencrypted Audiobook"; + AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync; + AsyncSteps["Step 2: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync; + AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync; } public override Task CancelAsync() { IsCanceled = true; - CloseInputFileStream(); + FinalizeDownload(); return Task.CompletedTask; } - protected bool Step_GetMetadata() - { - OnRetrievedCoverArt(null); - - return !IsCanceled; - } - - private bool Step_DownloadAudiobookAsSingleFile() + protected override async Task Step_DownloadAndDecryptAudiobookAsync() { DateTime startTime = DateTime.Now; // MUST put InputFileStream.Length first, because it starts background downloader. - while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled) + while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled) { var rate = InputFileStream.WritePosition / (DateTime.Now - startTime).TotalSeconds; @@ -109,14 +47,17 @@ namespace AaxDecrypter BytesReceived = (long)(InputFileStream.Length * progressPercent), TotalBytesToReceive = InputFileStream.Length }); - Thread.Sleep(200); + + await Task.Delay(200); } - CloseInputFileStream(); - - var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters); - SetOutputFileName(realOutputFileName); - OnFileCreated(realOutputFileName); + FinalizeDownload(); + + if (!IsCanceled) + { + FileUtility.SaferMove(InputFileStream.SaveFilePath, OutputFileName); + OnFileCreated(OutputFileName); + } return !IsCanceled; } diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj index 30905522..e486344b 100644 --- a/Source/DataLayer/DataLayer.csproj +++ b/Source/DataLayer/DataLayer.csproj @@ -10,7 +10,7 @@ - + all diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index d0010170..c6ea7e4b 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using AaxDecrypter; using ApplicationServices; -using AudibleApi; using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; @@ -138,41 +137,27 @@ namespace FileLiberator private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic) { - //I assume if ContentFormat == "MPEG" that 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. + //If DrmType != Adrm the delivered file is an unencrypted mp3. - 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) ? - OutputFormat.Mp3 : OutputFormat.M4b; + long chapterStartMs + = config.StripAudibleBrandAudio + ? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs + : 0; - long chapterStartMs = config.StripAudibleBrandAudio ? - contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0; - - var dlOptions = new DownloadOptions - ( - libraryBook, - contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, - Resources.USER_AGENT - ) + var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl) { AudibleKey = contentLic?.Voucher?.Key, AudibleIV = contentLic?.Voucher?.Iv, - 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), - ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), - FixupFile = config.AllowLibationFixup - }; + OutputFormat = outputFormat, + LameConfig = GetLameOptions(config), + ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), + RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0), + }; var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList(); @@ -277,8 +262,10 @@ namespace FileLiberator foreach (var c in chapters) { - if (c.Chapters is not null) - { + if (c.Chapters is null) + chaps.Add(c); + else + { if (c.LengthMs < 10000) { c.Chapters[0].StartOffsetMs = c.StartOffsetMs; @@ -296,8 +283,6 @@ namespace FileLiberator chaps.AddRange(children); c.Chapters = null; } - else - chaps.Add(c); } return chaps; } diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index c43d1bcd..2790d01e 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -3,7 +3,6 @@ using AAXClean; using Dinah.Core; using DataLayer; using LibationFileManager; -using FileManager; using System.Threading.Tasks; using System; using System.IO; @@ -17,36 +16,35 @@ namespace FileLiberator public LibraryBook LibraryBook { get; } public LibraryBookDto LibraryBookDto { get; } public string DownloadUrl { get; } - public string UserAgent { get; } public string AudibleKey { get; init; } public string AudibleIV { get; init; } - public AaxDecrypter.OutputFormat OutputFormat { get; init; } - public bool TrimOutputToChapterLength { 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 TimeSpan RuntimeLength { get; init; } + public OutputFormat OutputFormat { get; init; } public ChapterInfo ChapterInfo { get; init; } - public bool FixupFile { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; } - public bool Downsample { get; init; } - public bool MatchSourceBitrate { get; init; } - public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; - - public bool MoveMoovToBeginning { get; init; } + public string UserAgent => AudibleApi.Resources.USER_AGENT; + public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio; + public bool StripUnabridged => config.AllowLibationFixup && config.StripUnabridged; + public bool CreateCueSheet => config.CreateCueSheet; + 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) => Templates.ChapterFile.GetFilename(LibraryBookDto, props); - public string GetMultipartTitleName(MultiConvertFileProperties props) + public string GetMultipartTitle(MultiConvertFileProperties props) => Templates.ChapterTitle.GetTitle(LibraryBookDto, props); - public async Task SaveClipsAndBookmarks(string fileName) + public async Task SaveClipsAndBookmarksAsync(string fileName) { if (DownloadClipsBookmarks) { - var format = Configuration.Instance.ClipsBookmarksFileFormat; + var format = config.ClipsBookmarksFileFormat; var formatExtension = format.ToString().ToLowerInvariant(); var filePath = Path.ChangeExtension(fileName, formatExtension); @@ -71,20 +69,21 @@ namespace FileLiberator return string.Empty; } + private readonly Configuration config; private readonly IDisposable cancellation; 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)); 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 LibraryBookDto = LibraryBook.ToDto(); cancellation = - Configuration.Instance + config .ObservePropertyChanged( nameof(Configuration.DownloadSpeedLimit), newVal => DownloadSpeedChanged?.Invoke(this, newVal)); diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj index 17dab863..94a6136f 100644 --- a/Source/FileManager/FileManager.csproj +++ b/Source/FileManager/FileManager.csproj @@ -5,7 +5,7 @@ - + diff --git a/Source/FileManager/LongPath.cs b/Source/FileManager/LongPath.cs index 3c10a56f..06f1db6a 100644 --- a/Source/FileManager/LongPath.cs +++ b/Source/FileManager/LongPath.cs @@ -40,6 +40,7 @@ namespace FileManager } } + [JsonConstructor] private LongPath(string path) { if (IsWindows && path.Length > MaxPathLength) @@ -56,9 +57,8 @@ namespace FileManager ///a choice made by the linux kernel. As best as I can tell, pretty //much everyone uses UTF-8. public static int GetFilesystemStringLength(StringBuilder filename) - => LongPath.IsWindows ? - filename.Length - : Encoding.UTF8.GetByteCount(filename.ToString()); + => IsWindows ? filename.Length + : Encoding.UTF8.GetByteCount(filename.ToString()); public static implicit operator LongPath(string path) { diff --git a/Source/LibationFileManager/Configuration.PropertyChange.cs b/Source/LibationFileManager/Configuration.PropertyChange.cs index c2e0fa76..f6d70073 100644 --- a/Source/LibationFileManager/Configuration.PropertyChange.cs +++ b/Source/LibationFileManager/Configuration.PropertyChange.cs @@ -2,7 +2,7 @@ namespace LibationFileManager { - public partial class Configuration : PropertyChangeFilter + public partial class Configuration { /* * Use this type in the getter for any Dictionary settings, diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index d66d0c13..4747331c 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -7,7 +7,7 @@ using FileManager; namespace LibationFileManager { - public partial class Configuration + public partial class Configuration : PropertyChangeFilter { public bool LibationSettingsAreValid => File.Exists(APPSETTINGS_JSON) diff --git a/Source/LibationFileManager/PropertyChangeFilter.cs b/Source/LibationFileManager/PropertyChangeFilter.cs deleted file mode 100644 index 5fe0d6e4..00000000 --- a/Source/LibationFileManager/PropertyChangeFilter.cs +++ /dev/null @@ -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("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("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> propertyChangedActions = new(); - private readonly Dictionary> 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(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(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(MethodInfo methodInfo) where T : Attribute - => Attribute.GetCustomAttributes(methodInfo, typeof(T)) as T[]; - - #endregion - - #region Observables - - /// - /// Clear all subscriptions to PropertyChanged for - /// - public void ClearChangedSubscriptions(string propertyName) - { - if (propertyChangedActions.ContainsKey(propertyName) - && propertyChangedActions[propertyName] is not null) - propertyChangedActions[propertyName].Clear(); - } - - /// - /// Clear all subscriptions to PropertyChanging for - /// - public void ClearChangingSubscriptions(string propertyName) - { - if (propertyChangingActions.ContainsKey(propertyName) - && propertyChangingActions[propertyName] is not null) - propertyChangingActions[propertyName].Clear(); - } - - /// - /// Add an action to be executed when a property's value has changed - /// - /// The 's - /// Name of the property whose change triggers the - /// Action to be executed with the NewValue as a parameter - /// A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them. - public IDisposable ObservePropertyChanged(string propertyName, Action action) - { - validateSubscriber(propertyName, action); - - if (!propertyChangedActions.ContainsKey(propertyName)) - propertyChangedActions.Add(propertyName, new List()); - - var actionlist = propertyChangedActions[propertyName]; - - if (!actionlist.Contains(action)) - actionlist.Add(action); - - return new Unsubscriber(actionlist, action); - } - - /// - /// Add an action to be executed when a property's value is changing - /// - /// The 's - /// Name of the property whose change triggers the - /// Action to be executed with OldValue and NewValue as parameters - /// A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them. - public IDisposable ObservePropertyChanging(string propertyName, Action action) - { - validateSubscriber(propertyName, action); - - if (!propertyChangingActions.ContainsKey(propertyName)) - propertyChangingActions.Add(propertyName, new List()); - - var actionlist = propertyChangingActions[propertyName]; - - if (!actionlist.Contains(action)) - actionlist.Add(action); - - return new Unsubscriber(actionlist, action); - } - - private void validateSubscriber(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 _observers; - private Delegate _observer; - - internal Unsubscriber(List 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; - } - } -}