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() public override async Task CancelAsync()
{ {
IsCanceled = true; await base.CancelAsync();
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask); await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
FinalizeDownload();
} }
private Mp4File Open() 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(); protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
public virtual void SetCoverArt(byte[] coverArt) { } public virtual void SetCoverArt(byte[] coverArt) { }

View File

@ -17,13 +17,6 @@ namespace AaxDecrypter
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync; AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
} }
public override Task CancelAsync()
{
IsCanceled = true;
FinalizeDownload();
return Task.CompletedTask;
}
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync() protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{ {
await InputFileStream.DownloadTask; await InputFileStream.DownloadTask;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AaxDecrypter; using AaxDecrypter;
using ApplicationServices; using ApplicationServices;
@ -18,10 +19,15 @@ namespace FileLiberator
{ {
public override string Name => "Download & Decrypt"; public override string Name => "Download & Decrypt";
private AudiobookDownloadBase abDownloader; private AudiobookDownloadBase abDownloader;
private readonly CancellationTokenSource cancellationTokenSource = new();
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists(); public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
public override async Task CancelAsync()
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask; {
cancellationTokenSource.Cancel();
if (abDownloader is not null)
await abDownloader.CancelAsync();
}
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{ {
@ -41,8 +47,9 @@ namespace FileLiberator
} }
OnBegin(libraryBook); OnBegin(libraryBook);
var cancellationToken = cancellationTokenSource.Token;
try try
{ {
if (libraryBook.Book.Audio_Exists()) if (libraryBook.Book.Audio_Exists())
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
@ -50,7 +57,7 @@ namespace FileLiberator
downloadValidation(libraryBook); downloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync(); var api = await libraryBook.GetApiAsync();
var config = Configuration.Instance; 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; bool success = false;
try try
@ -74,38 +81,32 @@ namespace FileLiberator
.Where(f => f.FileType != FileType.AAXC) .Where(f => f.FileType != FileType.AAXC)
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path)))); .Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
return cancellationToken.ThrowIfCancellationRequested();
abDownloader?.IsCanceled is true return new StatusHandler { "Decrypt failed" };
? new StatusHandler { "Cancelled" }
: new StatusHandler { "Decrypt failed" };
} }
var finalStorageDir = getDestinationDirectory(libraryBook); var finalStorageDir = getDestinationDirectory(libraryBook);
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken));
Task[] finalTasks = Task[] finalTasks =
[ [
Task.Run(() => downloadCoverArt(downloadOptions)), Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)),
moveFilesTask, moveFilesTask,
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken))
]; ];
try try
{ {
await Task.WhenAll(finalTasks); await Task.WhenAll(finalTasks);
} }
catch catch when (!moveFilesTask.IsFaulted)
{ {
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions. //Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
//Only fail if the downloaded audio files failed to move to Books directory //Only fail if the downloaded audio files failed to move to Books directory
if (moveFilesTask.IsFaulted) }
{ finally
throw;
}
}
finally
{ {
if (moveFilesTask.IsCompletedSuccessfully) if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
{ {
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)); await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
@ -114,8 +115,12 @@ namespace FileLiberator
} }
return new StatusHandler(); return new StatusHandler();
} }
finally catch when (cancellationToken.IsCancellationRequested)
{
return new StatusHandler { "Cancelled" };
}
finally
{ {
OnCompleted(libraryBook); OnCompleted(libraryBook);
} }
@ -257,16 +262,17 @@ namespace FileLiberator
/// <summary>Move new files to 'Books' directory</summary> /// <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> /// <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 // create final directory. move each file into it
var destinationDir = getDestinationDirectory(libraryBook); var destinationDir = getDestinationDirectory(libraryBook);
cancellationToken.ThrowIfCancellationRequested();
for (var i = 0; i < entries.Count; i++) for (var i = 0; i < entries.Count; i++)
{ {
var entry = entries[i]; var entry = entries[i];
var realDest var realDest
= FileUtility.SaferMoveToValidPath( = FileUtility.SaferMoveToValidPath(
entry.Path, entry.Path,
Path.Combine(destinationDir, Path.GetFileName(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) // 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 }; entries[i] = entry with { Path = realDest };
} cancellationToken.ThrowIfCancellationRequested();
}
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue); var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
if (cue != default) if (cue != default)
@ -287,7 +294,8 @@ namespace FileLiberator
SetFileTime(libraryBook, cue.Path); SetFileTime(libraryBook, cue.Path);
} }
AudibleFileStorage.Audio.Refresh(); cancellationToken.ThrowIfCancellationRequested();
AudibleFileStorage.Audio.Refresh();
} }
private static string getDestinationDirectory(LibraryBook libraryBook) private static string getDestinationDirectory(LibraryBook libraryBook)
@ -301,7 +309,7 @@ namespace FileLiberator
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries) private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio); => 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; if (!Configuration.Instance.DownloadCoverArt) return;
@ -316,7 +324,7 @@ namespace FileLiberator
if (File.Exists(coverPath)) if (File.Exists(coverPath))
FileUtility.SaferDelete(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) if (picBytes.Length > 0)
{ {
File.WriteAllBytes(coverPath, picBytes); 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 //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."); 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.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable #nullable enable
@ -24,9 +25,10 @@ public partial class DownloadOptions
/// <summary> /// <summary>
/// Initiate an audiobook download from the audible api. /// Initiate an audiobook download from the audible api.
/// </summary> /// </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, //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 //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) if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo; license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
var options = BuildDownloadOptions(libraryBook, config, license); token.ThrowIfCancellationRequested();
return BuildDownloadOptions(libraryBook, config, license);
return options;
} }
private class LicenseInfo private class LicenseInfo
@ -57,16 +58,18 @@ public partial class DownloadOptions
=> voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)]; => 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; var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm) if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
{ {
token.ThrowIfCancellationRequested();
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
return new LicenseInfo(license); return new LicenseInfo(license);
} }
token.ThrowIfCancellationRequested();
try try
{ {
//try to request a widevine content license using the user's spatial audio settings //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); return new LicenseInfo(contentLic);
using var client = new HttpClient(); using var client = new HttpClient();
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse, token);
var dash = new MpegDash(mpdResponse.Content.ReadAsStream()); var dash = new MpegDash(mpdResponse.Content.ReadAsStream(token));
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri)) if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
throw new InvalidDataException("Failed to get mpeg-dash content download url."); 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.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable #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); return getPath(def);
} }
public static byte[] GetPictureSynchronously(PictureDefinition def) public static byte[] GetPictureSynchronously(PictureDefinition def, CancellationToken cancellationToken = default)
{ {
lock (cacheLocker) lock (cacheLocker)
{ {
@ -94,7 +95,7 @@ namespace LibationFileManager
var bytes var bytes
= File.Exists(path) = File.Exists(path)
? File.ReadAllBytes(path) ? File.ReadAllBytes(path)
: downloadBytes(def); : downloadBytes(def, cancellationToken);
cache[def] = bytes; cache[def] = bytes;
} }
return cache[def]; return cache[def];
@ -124,7 +125,7 @@ namespace LibationFileManager
} }
private static HttpClient imageDownloadClient { get; } = new HttpClient(); 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) if (def.PictureId is null)
return GetDefaultImage(def.Size); return GetDefaultImage(def.Size);
@ -132,7 +133,7 @@ namespace LibationFileManager
try try
{ {
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_"; 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 // save image file. make sure to not save default image
var path = getPath(def); var path = getPath(def);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationFileManager namespace LibationFileManager
@ -10,7 +11,7 @@ namespace LibationFileManager
public static class WindowsDirectory public static class WindowsDirectory
{ {
public static void SetCoverAsFolderIcon(string pictureId, string directory) public static void SetCoverAsFolderIcon(string pictureId, string directory, CancellationToken cancellationToken)
{ {
try try
{ {
@ -19,9 +20,8 @@ namespace LibationFileManager
return; return;
// get path of cover art in Images dir. Download first if not exists // 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);
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
} }
catch (Exception ex) catch (Exception ex)
{ {