Improve download/decrypt cancellation

This commit is contained in:
Michael Bucari-Tovo 2025-07-21 15:56:41 -06:00
parent 80b86086ca
commit 40b4915b65
7 changed files with 70 additions and 60 deletions

View File

@ -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()

View File

@ -120,7 +120,12 @@ namespace AaxDecrypter
}
}
public abstract Task CancelAsync();
public virtual Task CancelAsync()
{
IsCanceled = true;
FinalizeDownload();
return Task.CompletedTask;
}
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
public virtual void SetCoverArt(byte[] coverArt) { }

View File

@ -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<bool> Step_DownloadAndDecryptAudiobookAsync()
{
await InputFileStream.DownloadTask;

View File

@ -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 async Task CancelAsync()
{
cancellationTokenSource.Cancel();
if (abDownloader is not null)
await abDownloader.CancelAsync();
}
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
@ -41,6 +47,7 @@ namespace FileLiberator
}
OnBegin(libraryBook);
var cancellationToken = cancellationTokenSource.Token;
try
{
@ -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
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
{
if (moveFilesTask.IsCompletedSuccessfully)
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
{
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
@ -115,6 +116,10 @@ namespace FileLiberator
return new StatusHandler();
}
catch when (cancellationToken.IsCancellationRequested)
{
return new StatusHandler { "Cancelled" };
}
finally
{
OnCompleted(libraryBook);
@ -257,10 +262,11 @@ namespace FileLiberator
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> 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++)
{
@ -278,6 +284,7 @@ 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);
@ -287,6 +294,7 @@ namespace FileLiberator
SetFileTime(libraryBook, cue.Path);
}
cancellationToken.ThrowIfCancellationRequested();
AudibleFileStorage.Audio.Refresh();
}
@ -301,7 +309,7 @@ namespace FileLiberator
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> 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;
}
}
}

View File

@ -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
/// <summary>
/// Initiate an audiobook download from the audible api.
/// </summary>
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook)
public static async Task<DownloadOptions> 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<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
private static async Task<LicenseInfo> 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.");

View File

@ -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);

View File

@ -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,8 +20,7 @@ namespace LibationFileManager
return;
// get path of cover art in Images dir. Download first if not exists
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300));
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300), cancellationToken);
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
}
catch (Exception ex)