diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs index 8f350cb2..fb33474f 100644 --- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -25,9 +25,8 @@ namespace AaxDecrypter public override async Task CancelAsync() { - IsCanceled = true; + await base.CancelAsync(); await (AaxConversion?.CancelAsync() ?? Task.CompletedTask); - FinalizeDownload(); } private Mp4File Open() diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 355b3eca..c5389d29 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -120,7 +120,12 @@ namespace AaxDecrypter } } - public abstract Task CancelAsync(); + public virtual Task CancelAsync() + { + IsCanceled = true; + FinalizeDownload(); + return Task.CompletedTask; + } protected abstract Task Step_DownloadAndDecryptAudiobookAsync(); public virtual void SetCoverArt(byte[] coverArt) { } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index 200a4b5f..18c57054 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -17,13 +17,6 @@ namespace AaxDecrypter AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync; } - public override Task CancelAsync() - { - IsCanceled = true; - FinalizeDownload(); - return Task.CompletedTask; - } - protected override async Task Step_DownloadAndDecryptAudiobookAsync() { await InputFileStream.DownloadTask; diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 3ead2a9f..a349b497 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AaxDecrypter; using ApplicationServices; @@ -18,10 +19,15 @@ namespace FileLiberator { public override string Name => "Download & Decrypt"; private AudiobookDownloadBase abDownloader; + private readonly CancellationTokenSource cancellationTokenSource = new(); - public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); - - public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask; + public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); + public override async Task CancelAsync() + { + cancellationTokenSource.Cancel(); + if (abDownloader is not null) + await abDownloader.CancelAsync(); + } public override async Task ProcessAsync(LibraryBook libraryBook) { @@ -41,8 +47,9 @@ namespace FileLiberator } OnBegin(libraryBook); + var cancellationToken = cancellationTokenSource.Token; - try + try { if (libraryBook.Book.Audio_Exists()) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; @@ -50,7 +57,7 @@ namespace FileLiberator downloadValidation(libraryBook); var api = await libraryBook.GetApiAsync(); var config = Configuration.Instance; - using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook); + using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook, cancellationToken); bool success = false; try @@ -74,38 +81,32 @@ namespace FileLiberator .Where(f => f.FileType != FileType.AAXC) .Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path)))); - return - abDownloader?.IsCanceled is true - ? new StatusHandler { "Cancelled" } - : new StatusHandler { "Decrypt failed" }; + cancellationToken.ThrowIfCancellationRequested(); + return new StatusHandler { "Decrypt failed" }; } var finalStorageDir = getDestinationDirectory(libraryBook); - var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); + var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken)); Task[] finalTasks = [ - Task.Run(() => downloadCoverArt(downloadOptions)), + Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)), moveFilesTask, - Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) + Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken)) ]; try { - await Task.WhenAll(finalTasks); - } - catch - { + await Task.WhenAll(finalTasks); + } + catch when (!moveFilesTask.IsFaulted) + { //Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions. - //Only fail if the downloaded audio files failed to move to Books directory - if (moveFilesTask.IsFaulted) - { - throw; - } - } - finally + //Only fail if the downloaded audio files failed to move to Books directory + } + finally { - if (moveFilesTask.IsCompletedSuccessfully) + if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested) { await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)); @@ -114,8 +115,12 @@ namespace FileLiberator } return new StatusHandler(); - } - finally + } + catch when (cancellationToken.IsCancellationRequested) + { + return new StatusHandler { "Cancelled" }; + } + finally { OnCompleted(libraryBook); } @@ -257,16 +262,17 @@ namespace FileLiberator /// Move new files to 'Books' directory /// Return directory if audiobook file(s) were successfully created and can be located on disk. Else null. - private static void moveFilesToBooksDir(LibraryBook libraryBook, List entries) + private static void moveFilesToBooksDir(LibraryBook libraryBook, List entries, CancellationToken cancellationToken) { // create final directory. move each file into it var destinationDir = getDestinationDirectory(libraryBook); + cancellationToken.ThrowIfCancellationRequested(); for (var i = 0; i < entries.Count; i++) { var entry = entries[i]; - var realDest + var realDest = FileUtility.SaferMoveToValidPath( entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), @@ -278,7 +284,8 @@ namespace FileLiberator // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) entries[i] = entry with { Path = realDest }; - } + cancellationToken.ThrowIfCancellationRequested(); + } var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue); if (cue != default) @@ -287,7 +294,8 @@ namespace FileLiberator SetFileTime(libraryBook, cue.Path); } - AudibleFileStorage.Audio.Refresh(); + cancellationToken.ThrowIfCancellationRequested(); + AudibleFileStorage.Audio.Refresh(); } private static string getDestinationDirectory(LibraryBook libraryBook) @@ -301,7 +309,7 @@ namespace FileLiberator private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable entries) => entries.FirstOrDefault(f => f.FileType == FileType.Audio); - private static void downloadCoverArt(DownloadOptions options) + private static void downloadCoverArt(DownloadOptions options, CancellationToken cancellationToken) { if (!Configuration.Instance.DownloadCoverArt) return; @@ -316,7 +324,7 @@ namespace FileLiberator if (File.Exists(coverPath)) FileUtility.SaferDelete(coverPath); - var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native)); + var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken); if (picBytes.Length > 0) { File.WriteAllBytes(coverPath, picBytes); @@ -327,6 +335,7 @@ namespace FileLiberator { //Failure to download cover art should not be considered a failure to download the book Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product."); + throw; } } } diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index 94368534..95fdacb7 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; #nullable enable @@ -24,9 +25,10 @@ public partial class DownloadOptions /// /// Initiate an audiobook download from the audible api. /// - public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook) + public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token) { - var license = await ChooseContent(api, libraryBook, config); + var license = await ChooseContent(api, libraryBook, config, token); + token.ThrowIfCancellationRequested(); //Some audiobooks will have incorrect chapters in the metadata returned from the license request, //but the metadata returned by the content metadata endpoint will be correct. Call the content @@ -36,9 +38,8 @@ public partial class DownloadOptions if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs) license.ContentMetadata.ChapterInfo = metadata.ChapterInfo; - var options = BuildDownloadOptions(libraryBook, config, license); - - return options; + token.ThrowIfCancellationRequested(); + return BuildDownloadOptions(libraryBook, config, license); } private class LicenseInfo @@ -57,16 +58,18 @@ public partial class DownloadOptions => voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)]; } - private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config) + private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token) { var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High; if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm) { + token.ThrowIfCancellationRequested(); var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); return new LicenseInfo(license); } + token.ThrowIfCancellationRequested(); try { //try to request a widevine content license using the user's spatial audio settings @@ -85,8 +88,8 @@ public partial class DownloadOptions return new LicenseInfo(contentLic); using var client = new HttpClient(); - using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); - var dash = new MpegDash(mpdResponse.Content.ReadAsStream()); + using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse, token); + var dash = new MpegDash(mpdResponse.Content.ReadAsStream(token)); if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri)) throw new InvalidDataException("Failed to get mpeg-dash content download url."); diff --git a/Source/LibationFileManager/PictureStorage.cs b/Source/LibationFileManager/PictureStorage.cs index d72c3614..d07bcf79 100644 --- a/Source/LibationFileManager/PictureStorage.cs +++ b/Source/LibationFileManager/PictureStorage.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; #nullable enable @@ -78,13 +79,13 @@ namespace LibationFileManager } } - public static string GetPicturePathSynchronously(PictureDefinition def) + public static string GetPicturePathSynchronously(PictureDefinition def, CancellationToken cancellationToken = default) { - GetPictureSynchronously(def); + GetPictureSynchronously(def, cancellationToken); return getPath(def); } - public static byte[] GetPictureSynchronously(PictureDefinition def) + public static byte[] GetPictureSynchronously(PictureDefinition def, CancellationToken cancellationToken = default) { lock (cacheLocker) { @@ -94,7 +95,7 @@ namespace LibationFileManager var bytes = File.Exists(path) ? File.ReadAllBytes(path) - : downloadBytes(def); + : downloadBytes(def, cancellationToken); cache[def] = bytes; } return cache[def]; @@ -124,7 +125,7 @@ namespace LibationFileManager } private static HttpClient imageDownloadClient { get; } = new HttpClient(); - private static byte[] downloadBytes(PictureDefinition def) + private static byte[] downloadBytes(PictureDefinition def, CancellationToken cancellationToken = default) { if (def.PictureId is null) return GetDefaultImage(def.Size); @@ -132,7 +133,7 @@ namespace LibationFileManager try { var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_"; - var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg").Result; + var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg", cancellationToken).Result; // save image file. make sure to not save default image var path = getPath(def); diff --git a/Source/LibationFileManager/WindowsDirectory.cs b/Source/LibationFileManager/WindowsDirectory.cs index 9d881aeb..e2c94240 100644 --- a/Source/LibationFileManager/WindowsDirectory.cs +++ b/Source/LibationFileManager/WindowsDirectory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace LibationFileManager @@ -10,7 +11,7 @@ namespace LibationFileManager public static class WindowsDirectory { - public static void SetCoverAsFolderIcon(string pictureId, string directory) + public static void SetCoverAsFolderIcon(string pictureId, string directory, CancellationToken cancellationToken) { try { @@ -19,9 +20,8 @@ namespace LibationFileManager return; // get path of cover art in Images dir. Download first if not exists - var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300)); - - InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory); + var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300), cancellationToken); + InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory); } catch (Exception ex) {