From 05f25a88c6ec66faab1367daf565b30761897b71 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Sun, 8 May 2022 11:08:23 -0600 Subject: [PATCH] Fix temp file reuse/cleanup. Add retain aax option. --- AaxDecrypter/AaxcDownloadConvertBase.cs | 6 +-- AaxDecrypter/AaxcDownloadMultiConverter.cs | 24 ++++------ AaxDecrypter/AaxcDownloadSingleConverter.cs | 13 ++--- AaxDecrypter/AudiobookDownloadBase.cs | 48 ++++++++++++++----- ...{DownloadLicense.cs => DownloadOptions.cs} | 5 +- .../UnencryptedAudiobookDownloader.cs | 2 +- AppScaffolding/LibationScaffolding.cs | 3 ++ FileLiberator/AudioFileStorageExt.cs | 2 +- FileLiberator/DownloadDecryptBook.cs | 23 +++++---- FileManager/FileNamingTemplate.cs | 4 +- FileManager/FileUtility.cs | 4 +- LibationFileManager/Configuration.cs | 7 +++ LibationFileManager/Templates.cs | 4 +- 13 files changed, 90 insertions(+), 55 deletions(-) rename AaxDecrypter/{DownloadLicense.cs => DownloadOptions.cs} (82%) diff --git a/AaxDecrypter/AaxcDownloadConvertBase.cs b/AaxDecrypter/AaxcDownloadConvertBase.cs index 4784be37..1567d8b3 100644 --- a/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -10,7 +10,7 @@ namespace AaxDecrypter protected AaxFile AaxFile; - protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadLicense dlLic) + protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadOptions dlLic) : base(outFileName, cacheDirectory, dlLic) { } /// Setting cover art by this method will insert the art into the audiobook metadata @@ -46,7 +46,7 @@ namespace AaxDecrypter OnDecryptProgressUpdate(zeroProgress); - AaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV); + AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV); return zeroProgress; } @@ -68,7 +68,7 @@ namespace AaxDecrypter if (double.IsNormal(estTimeRemaining)) OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining)); - var progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds; + var progressPercent = (e.ProcessPosition / e.TotalDuration); OnDecryptProgressUpdate( new DownloadProgress diff --git a/AaxDecrypter/AaxcDownloadMultiConverter.cs b/AaxDecrypter/AaxcDownloadMultiConverter.cs index ba4dc68c..82e8d047 100644 --- a/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -18,13 +18,13 @@ namespace AaxDecrypter private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3); private List multiPartFilePaths { get; } = new List(); - public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, + public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic, Func multipartFileNameCallback = null) : base(outFileName, cacheDirectory, dlLic) { Steps = new StepSequence { - Name = "Download and Convert Aaxc To " + DownloadLicense.OutputFormat, + Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat, ["Step 1: Get Aaxc Metadata"] = Step_GetMetadata, ["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter, @@ -61,10 +61,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea { var zeroProgress = Step_DownloadAudiobook_Start(); - var chapters = DownloadLicense.ChapterInfo.Chapters.ToList(); + var chapters = DownloadOptions.ChapterInfo.Chapters.ToList(); // Ensure split files are at least minChapterLength in duration. - var splitChapters = new ChapterInfo(DownloadLicense.ChapterInfo.StartOffset); + var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset); var runningTotal = TimeSpan.Zero; string title = ""; @@ -89,7 +89,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea ConversionResult result; AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; - if (DownloadLicense.OutputFormat == OutputFormat.M4b) + if (DownloadOptions.OutputFormat == OutputFormat.M4b) result = ConvertToMultiMp4a(splitChapters); else result = ConvertToMultiMp3(splitChapters); @@ -97,13 +97,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea Step_DownloadAudiobook_End(zeroProgress); - var success = result == ConversionResult.NoErrorsDetected; - - if (success) - foreach (var path in multiPartFilePaths) - OnFileCreated(path); - - return success; + return result == ConversionResult.NoErrorsDetected; } private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters) @@ -111,7 +105,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea var chapterCount = 0; return AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback => createOutputFileStream(++chapterCount, splitChapters, newSplitCallback), - DownloadLicense.TrimOutputToChapterLength); + DownloadOptions.TrimOutputToChapterLength); } private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters) @@ -121,7 +115,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea { createOutputFileStream(++chapterCount, splitChapters, newSplitCallback); ((NAudio.Lame.LameConfig)newSplitCallback.UserState).ID3.Track = chapterCount.ToString(); - }, null, DownloadLicense.TrimOutputToChapterLength); + }, null, DownloadOptions.TrimOutputToChapterLength); } private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback) @@ -140,6 +134,8 @@ That naming may not be desirable for everyone, but it's an easy change to instea FileUtility.SaferDelete(fileName); newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); + + OnFileCreated(fileName); } } } diff --git a/AaxDecrypter/AaxcDownloadSingleConverter.cs b/AaxDecrypter/AaxcDownloadSingleConverter.cs index 0592d108..e9492d20 100644 --- a/AaxDecrypter/AaxcDownloadSingleConverter.cs +++ b/AaxDecrypter/AaxcDownloadSingleConverter.cs @@ -11,12 +11,12 @@ namespace AaxDecrypter { protected override StepSequence Steps { get; } - public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic) + public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic) : base(outFileName, cacheDirectory, dlLic) { Steps = new StepSequence { - Name = "Download and Convert Aaxc To " + DownloadLicense.OutputFormat, + Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat, ["Step 1: Get Aaxc Metadata"] = Step_GetMetadata, ["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile, @@ -32,15 +32,16 @@ namespace AaxDecrypter FileUtility.SaferDelete(OutputFileName); var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); + OnFileCreated(OutputFileName); AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; var decryptionResult - = DownloadLicense.OutputFormat == OutputFormat.M4b - ? AaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo, DownloadLicense.TrimOutputToChapterLength) - : AaxFile.ConvertToMp3(outputFile, null, DownloadLicense.ChapterInfo, DownloadLicense.TrimOutputToChapterLength); + = DownloadOptions.OutputFormat == OutputFormat.M4b + ? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength) + : AaxFile.ConvertToMp3(outputFile, null, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength); AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; - DownloadLicense.ChapterInfo = AaxFile.Chapters; + DownloadOptions.ChapterInfo = AaxFile.Chapters; Step_DownloadAudiobook_End(zeroProgress); diff --git a/AaxDecrypter/AudiobookDownloadBase.cs b/AaxDecrypter/AudiobookDownloadBase.cs index 80ab189d..0de90a27 100644 --- a/AaxDecrypter/AudiobookDownloadBase.cs +++ b/AaxDecrypter/AudiobookDownloadBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using Dinah.Core; using Dinah.Core.Net.Http; @@ -20,8 +21,9 @@ namespace AaxDecrypter public event EventHandler FileCreated; protected bool IsCanceled { get; set; } + protected string OutputFileName { get; private set; } - protected DownloadLicense DownloadLicense { get; } + protected DownloadOptions DownloadOptions { get; } protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream; // Don't give the property a 'set'. This should have to be an obvious choice; not accidental @@ -31,9 +33,9 @@ namespace AaxDecrypter private NetworkFileStreamPersister nfsPersister; private string jsonDownloadState { get; } - private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp"); + public string TempFilePath { get; } - protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic) + protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic) { OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName)); @@ -43,9 +45,11 @@ namespace AaxDecrypter if (!Directory.Exists(cacheDirectory)) throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}"); - jsonDownloadState = Path.Combine(cacheDirectory, Path.ChangeExtension(OutputFileName, ".json")); - DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic)); + jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json"))); + TempFilePath = Path.ChangeExtension(jsonDownloadState, ".tmp"); + + DownloadOptions = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic)); // delete file after validation is complete FileUtility.SaferDelete(OutputFileName); @@ -99,7 +103,7 @@ namespace AaxDecrypter { var path = Path.ChangeExtension(OutputFileName, ".cue"); path = FileUtility.GetValidFilename(path); - File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo)); + File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); OnFileCreated(path); } catch (Exception ex) @@ -111,9 +115,27 @@ namespace AaxDecrypter protected bool Step_Cleanup() { - FileUtility.SaferDelete(jsonDownloadState); - FileUtility.SaferDelete(tempFile); - return !IsCanceled; + bool success = !IsCanceled; + if (success) + { + FileUtility.SaferDelete(jsonDownloadState); + + if (DownloadOptions.RetainEncryptedFile) + { + string aaxPath = Path.ChangeExtension(TempFilePath, ".aax"); + FileUtility.SaferMove(TempFilePath, aaxPath); + OnFileCreated(aaxPath); + } + else + FileUtility.SaferDelete(TempFilePath); + } + else + { + FileUtility.SaferDelete(OutputFileName); + } + + + return success; } private NetworkFileStreamPersister OpenNetworkFileStream() @@ -126,13 +148,13 @@ namespace AaxDecrypter var 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. - nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadLicense.DownloadUrl)); + nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl)); return nfsp; } catch { FileUtility.SaferDelete(jsonDownloadState); - FileUtility.SaferDelete(tempFile); + FileUtility.SaferDelete(TempFilePath); return NewNetworkFilePersister(); } } @@ -141,10 +163,10 @@ namespace AaxDecrypter { var headers = new System.Net.WebHeaderCollection { - { "User-Agent", DownloadLicense.UserAgent } + { "User-Agent", DownloadOptions.UserAgent } }; - var networkFileStream = new NetworkFileStream(tempFile, new Uri(DownloadLicense.DownloadUrl), 0, headers); + var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers); return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); } } diff --git a/AaxDecrypter/DownloadLicense.cs b/AaxDecrypter/DownloadOptions.cs similarity index 82% rename from AaxDecrypter/DownloadLicense.cs rename to AaxDecrypter/DownloadOptions.cs index 51b69c71..6105b1ed 100644 --- a/AaxDecrypter/DownloadLicense.cs +++ b/AaxDecrypter/DownloadOptions.cs @@ -3,7 +3,7 @@ using Dinah.Core; namespace AaxDecrypter { - public class DownloadLicense + public class DownloadOptions { public string DownloadUrl { get; } public string UserAgent { get; } @@ -11,9 +11,10 @@ namespace AaxDecrypter public string AudibleIV { get; init; } public OutputFormat OutputFormat { get; init; } public bool TrimOutputToChapterLength { get; init; } + public bool RetainEncryptedFile { get; init; } public ChapterInfo ChapterInfo { get; set; } - public DownloadLicense(string downloadUrl, string userAgent) + public DownloadOptions(string downloadUrl, string userAgent) { DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); diff --git a/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/AaxDecrypter/UnencryptedAudiobookDownloader.cs index 108a662f..885e839b 100644 --- a/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -10,7 +10,7 @@ namespace AaxDecrypter { protected override StepSequence Steps { get; } - public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic) + public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadOptions dlLic) : base(outFileName, cacheDirectory, dlLic) { Steps = new StepSequence diff --git a/AppScaffolding/LibationScaffolding.cs b/AppScaffolding/LibationScaffolding.cs index 2ce6611b..1317a097 100644 --- a/AppScaffolding/LibationScaffolding.cs +++ b/AppScaffolding/LibationScaffolding.cs @@ -83,6 +83,9 @@ namespace AppScaffolding if (!config.Exists(nameof(config.StripAudibleBrandAudio))) config.StripAudibleBrandAudio = false; + + if (!config.Exists(nameof(config.RetainAaxFile))) + config.RetainAaxFile = false; if (!config.Exists(nameof(config.FolderTemplate))) config.FolderTemplate = Templates.Folder.DefaultTemplate; diff --git a/FileLiberator/AudioFileStorageExt.cs b/FileLiberator/AudioFileStorageExt.cs index a58bf648..049efaed 100644 --- a/FileLiberator/AudioFileStorageExt.cs +++ b/FileLiberator/AudioFileStorageExt.cs @@ -39,7 +39,7 @@ namespace FileLiberator /// File name: final file name. /// public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension) - => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension); + => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true); /// /// PDF: audio file does not exist diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index 339a2c4b..654936f8 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -61,7 +61,12 @@ namespace FileLiberator // decrypt failed if (!success) + { + foreach (var tmpFile in entries) + FileUtility.SaferDelete(tmpFile.Path); + return new StatusHandler { "Decrypt failed" }; + } // moves new files from temp dir to final dest var movedAudioFile = moveFilesToBooksDir(libraryBook, entries); @@ -92,7 +97,7 @@ namespace FileLiberator var api = await libraryBook.GetApiAsync(); var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); - var audiobookDlLic = BuildDownloadLicense(config, contentLic); + var audiobookDlLic = BuildDownloadOptions(config, contentLic); var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.OutputFormat.ToString().ToLower()); var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; @@ -132,28 +137,28 @@ namespace FileLiberator } } - private static DownloadLicense BuildDownloadLicense(Configuration config, AudibleApi.Common.ContentLicense contentLic) + private static DownloadOptions BuildDownloadOptions(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. - var outputFormat = - contentLic?.ContentMetadata?.ContentReference?.ContentFormat == "MPEG" || - (config.AllowLibationFixup && config.DecryptToLossy) ? + + bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm; + + var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ? OutputFormat.Mp3 : OutputFormat.M4b; - var audiobookDlLic = new DownloadLicense + var audiobookDlLic = new DownloadOptions ( contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl, Resources.USER_AGENT - ) { - AudibleKey = contentLic?.Voucher?.Key, AudibleIV = contentLic?.Voucher?.Iv, OutputFormat = outputFormat, - TrimOutputToChapterLength = config.StripAudibleBrandAudio + TrimOutputToChapterLength = config.StripAudibleBrandAudio, + RetainEncryptedFile = config.RetainAaxFile && encrypted }; if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3) diff --git a/FileManager/FileNamingTemplate.cs b/FileManager/FileNamingTemplate.cs index 0b4dca26..1dba7e0f 100644 --- a/FileManager/FileNamingTemplate.cs +++ b/FileManager/FileNamingTemplate.cs @@ -29,14 +29,14 @@ namespace FileManager public string IllegalCharacterReplacements { get; set; } /// Generate a valid path for this file or directory - public string GetFilePath() + public string GetFilePath(bool returnFirstExisting = false) { var filename = Template; foreach (var r in ParameterReplacements) filename = filename.Replace($"<{formatKey(r.Key)}>", formatValue(r.Value)); - return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements); + return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements, returnFirstExisting); } private static string formatKey(string key) diff --git a/FileManager/FileUtility.cs b/FileManager/FileUtility.cs index 025c63a8..61d44847 100644 --- a/FileManager/FileUtility.cs +++ b/FileManager/FileUtility.cs @@ -48,7 +48,7 @@ namespace FileManager ///
- ensure uniqueness ///
- enforce max file length ///
- public static string GetValidFilename(string path, string illegalCharacterReplacements = "") + public static string GetValidFilename(string path, string illegalCharacterReplacements = "", bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(path, nameof(path)); @@ -69,7 +69,7 @@ namespace FileManager fullfilename = removeInvalidWhitespace(fullfilename); var i = 0; - while (File.Exists(fullfilename)) + while (File.Exists(fullfilename) && !returnFirstExisting) { var increm = $" ({++i})"; fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension; diff --git a/LibationFileManager/Configuration.cs b/LibationFileManager/Configuration.cs index ae872e2c..767973fc 100644 --- a/LibationFileManager/Configuration.cs +++ b/LibationFileManager/Configuration.cs @@ -116,6 +116,13 @@ namespace LibationFileManager get => persistentDictionary.GetNonString(nameof(SplitFilesByChapter)); set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value); } + + [Description("Retain the Aax file after successfully decrypting")] + public bool RetainAaxFile + { + get => persistentDictionary.GetNonString(nameof(RetainAaxFile)); + set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value); + } public enum BadBookAction { diff --git a/LibationFileManager/Templates.cs b/LibationFileManager/Templates.cs index 4c427de5..b2826c15 100644 --- a/LibationFileManager/Templates.cs +++ b/LibationFileManager/Templates.cs @@ -230,9 +230,9 @@ namespace LibationFileManager #region to file name /// USES LIVE CONFIGURATION VALUES - public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension) + public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) - .GetFilePath(); + .GetFilePath(returnFirstExisting); #endregion }